# Back-to-Top Button — Design Spec

**Date:** 2026-04-20
**Status:** Approved

---

## Context

The TODO list includes "add back-to-top button". Article pages can be long and users currently have no quick way to return to the top. The button should integrate cleanly with the existing hacker-aesthetic theme, follow Alpine.js patterns already in use, and meet WCAG 2.1 AA accessibility standards.

---

## Design Decisions

| Decision | Choice | Rationale |
|---|---|---|
| Position | Fixed bottom-right | Conventional, expected UX |
| Shape | Circle solid | Consistent with theme energy; always-visible purple matches accent |
| Trigger | 33% scroll depth | Balanced — not too early, not too late |
| Animation | Slide up + fade (300ms) | Reuses existing `slideUp` keyframe |
| Pages | `.Kind "page"` only | Articles and singles — same condition as reading progress bar |
| Implementation | Inline Alpine.js in a partial | Consistent with `social-share.html`, `header.html` patterns |

---

## Behaviour

- **Appears** when `window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) >= 0.33`
- **Disappears** when scroll drops back below 33%
- **On click**: smooth-scrolls to top (`window.scrollTo({ top: 0, behavior: 'smooth' })`)
- **Reduced motion**: CSS global `prefers-reduced-motion` rule already suppresses all animations; the button still appears/disappears (via Alpine `x-show`) but without the slide animation. The `smooth` scroll behavior should also be skipped — handled in the click handler by checking `window.matchMedia('(prefers-reduced-motion: reduce)').matches`.
- **z-index**: `z-40` — sits below toasts (`z-50`), modals (`z-50`), and reading progress bar (`z-9999`). Toasts stack above it naturally.

---

## Visual Spec

```
Position:  fixed bottom-6 right-6  (24px from edges)
Size:      44×44px circle
Default:   bg: var(--accent) #a855f7, glow: box-shadow 0 0 12px rgba(168,85,247,0.4)
Hover:     bg: #9333ea (one shade darker), glow: box-shadow 0 0 20px var(--accent)
Icon:      Feather Icons `chevron-up` (already loaded globally via CDN)
Entrance:  slideUp keyframe, 300ms ease-out
Exit:      opacity fade (Alpine x-show transition), 200ms
```

---

## Accessibility

- `aria-label="Back to top"` on the button element
- `role="button"` implicit (native `<button>`)
- Keyboard accessible: Tab-focusable, Enter/Space triggers scroll
- Focus ring: `focus-visible` outline using `var(--accent)` (already defined globally in CSS)
- Button is removed from tab order when hidden via Alpine `x-show` (Alpine removes from DOM flow when not visible)
- WCAG 2.1 AA contrast: white `↑` icon on `#a855f7` purple background — passes AA for UI components

---

## Implementation

### New files
- `themes/danix-xyz-hacker/layouts/partials/back-to-top.html` — the button partial

### Modified files
- `themes/danix-xyz-hacker/layouts/_default/baseof.html` — include partial + JS, conditionally on `.Kind "page"`
- `themes/danix-xyz-hacker/assets/css/main.css` — add `.back-to-top` component class

### No new JS file needed
Logic is simple enough for inline Alpine `x-data` — consistent with `social-share.html`. No separate `.js` asset required.

---

## Component Structure

**`back-to-top.html` partial:**
```html
<div
  x-data="{ visible: false }"
  @scroll.window="visible = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) >= 0.33"
>
  <button
    x-show="visible"
    x-transition:enter="..."
    x-transition:enter-start="..."
    x-transition:enter-end="..."
    x-transition:leave="..."
    x-transition:leave-start="..."
    x-transition:leave-end="..."
    class="back-to-top"
    aria-label="Back to top"
    @click="window.scrollTo({ top: 0, behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' })"
  >
    <!-- feather chevron-up icon -->
  </button>
</div>
```

**`.back-to-top` CSS class** (added to `main.css` `@layer components`):
```css
.back-to-top {
  @apply fixed bottom-6 right-6 z-40 w-11 h-11 rounded-full flex items-center justify-center;
  background: var(--accent);
  box-shadow: 0 0 12px rgba(168, 85, 247, 0.4);
  transition: background 200ms ease, box-shadow 200ms ease;
  color: #fff;
}
.back-to-top:hover {
  background: #9333ea;
  box-shadow: 0 0 20px var(--accent);
}
.back-to-top:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
```

---

## Verification

1. Start Hugo dev server (`hugo server`)
2. Open any article page
3. Scroll past 33% — button slides up from bottom-right
4. Scroll back up — button disappears
5. Click button — smooth scroll to top
6. Tab to button — focus ring visible
7. Press Enter while focused — smooth scroll to top
8. Test with `prefers-reduced-motion: reduce` in browser devtools — button appears without animation, click scrolls instantly
9. Confirm toasts (if triggered) render above the button
10. Check on mobile (narrow viewport) — button sits at bottom-right without overlapping content
