Mobile / touch · M.1
Thumb-zone fixed action bar
The handle on a suitcase, moved to where the hand already is.
The page's one primary action, pinned to the bottom of the viewport inside the arc a thumb can reach without regripping the phone.
4 knobs
How it actually works
On a phone the primary action is usually the hardest thing on the page to reach: it is folded into a hamburger at the top, or it is a phone number at the bottom of a 4,800px page. Both are a full regrip away. This is the cheapest conversion fix on a phone layout and it costs one fixed element. It is also the one that most often ships broken, because the containing-block trap and the safe-area inset are both invisible on a desktop browser and on most emulators.
position: fixed with bottom: 0, and the bar's own bottom padding built out of env(safe-area-inset-bottom) so the last row of pixels is not underneath the home indicator on a notched phone. The body gets matching bottom padding so the final paragraph of the page can still be read rather than sitting permanently behind the bar. The trap that is not obvious: an ancestor carrying backdrop-filter becomes the containing block for every fixed descendant, so a bar declared inside a blurred header pins to the header instead of to the viewport and nothing about the CSS looks wrong. Hide-on-scroll-down is a transform on the bar driven by scroll direction, never a display swap.
The knobs, named
Bar height, bottom offset, hide-on-scroll-down, safe-area inset. Only the safe-area knob is sourced: env(safe-area-inset-bottom) is a named CSS environment variable and the knob switches that exact parameter. The other three are our design against our own shipped build.
| Knob | Source | What it teaches |
|---|---|---|
| Bar height | ours | The deck the action sits on. Below 44 the button inside it cannot meet the target-size floor, which is the number this knob is really about. |
| Bottom offset | ours | Gap between the bar and the bottom edge. Zero reads as part of the phone; large reads as a floating pill and starts costing thumb travel. |
| Hide on scroll down | ours | Reading gets the whole screen, and the bar comes back the instant you scroll up. The cost is that the action is not always there, which is the entire reason the bar exists. |
| Safe-area inset | sourced | env(safe-area-inset-bottom). Off, the bar's last rows sit under the home indicator on a notched phone and the button is partly untappable. The simulated inset here is drawn hatched. |
sourced means the source names this parameter. ours means the source names none and the knob is our design against the mechanism. No knob here is invented and passed off as sourced.
Evidence
VERIFIED (ours, shipped)
Our own shipped code: clients/harrison-handyman/craft.css, section 4 "THE CONTACT ACTION BAR", 2026-07-21: the nav CTA leaves the collapsed drawer below 760px and becomes a full-width bar at bottom: 0 with min-height: calc(52px + 1.1rem + env(safe-area-inset-bottom, 0px)) and body { padding-bottom: 5.4rem }. The backdrop-filter containing-block trap is documented in that same file, in the comment that begins "A backdrop-filter makes an element the CONTAINING BLOCK". env(safe-area-inset-bottom) is named by the CSS Environment Variables spec. Nothing here is claimed beyond those two things.
- Seen on
- Our own harrison-handyman build (shipped). The pattern is near-ubiquitous in native apps and in commerce sites; we did not render one to confirm it, so no third party is cited.
- Dependencies
- vanilla CSS
- Difficulty
- easy to write, easy to ship broken
- Performance
- One fixed element. Hide-on-scroll is a transform on that one element, read in the read pass and written in the write pass, so it never reads a rect and writes a style in the same breath.
- Accessibility and the floor
- The bar is a real link or button in the document, not a floating div with a click handler, so it is reachable by keyboard and announced in order. Under .anim-gating hide-on-scroll never engages and the bar is simply always there, which is the safer of the two states.
Notes
Composability. It owns the bottom of the viewport, so nothing else may: no cookie bar, no chat bubble, no back-to-top. Pair it with the target-size floor (M.5), which is the rule the button inside it has to pass.