summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-04-20-back-to-top-button-design.md
blob: bc5959f9235bad6c4165154ff9f072d76264f529 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# 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