Nº 316 of 520
Mobile website bottom bar
A five-item bottom bar for a phone website with a raised square Book action, a scroll-linked active state, and hide-on-scroll-down that returns on scroll up.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Mobile website bottom bar" (piece mobile-web-bottom-nav) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 316 · "Mobile website bottom bar" · www.designlounge.live -->
# Mobile website bottom bar
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
A bottom navigation bar for a phone website, not a native app. The site is "Depot 41", a strength gym in an old tram depot in Porto. Five items sit in a 64px bar with a 2px black top rule: Home, Coaches, Book, Prices, Visit. Book is a raised 60px safety-orange square with a hard black offset shadow. The active item gets a 4px orange mark on the top rule and follows the section you are reading. The bar hides when you scroll down and comes back when you scroll up or reach the end. The detail worth copying is that it behaves like a website: items are anchor links to page sections, and it sits above the browser's own bottom toolbar.
### When to use it
- Use it for sites people come back to often on a phone: a gym timetable, a restaurant that takes orders, a booking site, a club, a PWA.
- Use it when there are three to five places people jump between, and one action that matters most (Book, Order).
- Do not use it on a marketing page, a portfolio, a launch page or an article. People read those once from top to bottom. Use a header with a hamburger there (`hamburger-circle-reveal` or `mobile-fullscreen-menu`).
- Do not use it with a sticky bottom call-to-action bar (`cta-sticky-mobile-bar`) on the same page. Pick one.
## Reference behaviour
1. First frame: the page is at the top. The bar is visible. Home has `aria-current="true"` and the orange mark.
2. Scroll down more than 6px past 80px from the top: the bar slides down out of view over 220ms `cubic-bezier(.4,0,1,1)`.
3. Scroll up more than 6px: the bar slides back over 280ms `cubic-bezier(.16,1,.3,1)`.
4. Reach the end of the page (within 8px): the bar comes back, even if you were scrolling down, and Visit becomes current.
5. Scroll position under 80px: the bar is always shown.
6. While scrolling, the current item is the last section whose top is above 40% of the screen height. Its mark grows from `scaleX(0)` to `scaleX(1)` over 160ms.
7. Tap an item: the page smooth-scrolls to that section. The current item updates as the section arrives.
8. Tap Book: the page scrolls to "Today", the class list. When Book is current, its label gets a 2px orange underline.
9. Press Book: the orange square moves 3px right and 3px down and its shadow shrinks from 4px to 1px, like a key being pressed.
10. Tab into the bar while it is hidden: it slides back so focus is never on something off screen.
11. In the class list, "Book" buttons toggle to "Booked" (`aria-pressed`). "Full" is disabled.
## Structure
```
390 x 844 (54px status reserve on top, 84px browser bar drawn over the bottom)
+--------------------------------------+
| DEPOT[41] [] Open until 22:00 | 56px, 2px black rule under
| LIFT HEAVY. | 64px condensed 800
| COME BACK TOMORROW. |
| ...lede, 3 fact cells... |
… 219 more lines. Copy or download for the full brief. Loading the source…Brief
259 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
A bottom navigation bar for a phone website, not a native app. The site is “Depot 41”, a strength gym in an old tram depot in Porto. Five items sit in a 64px bar with a 2px black top rule: Home, Coaches, Book, Prices, Visit. Book is a raised 60px safety-orange square with a hard black offset shadow. The active item gets a 4px orange mark on the top rule and follows the section you are reading. The bar hides when you scroll down and comes back when you scroll up or reach the end. The detail worth copying is that it behaves like a website: items are anchor links to page sections, and it sits above the browser’s own bottom toolbar.
When to use it
- Use it for sites people come back to often on a phone: a gym timetable, a restaurant that takes orders, a booking site, a club, a PWA.
- Use it when there are three to five places people jump between, and one action that matters most (Book, Order).
- Do not use it on a marketing page, a portfolio, a launch page or an article. People read those once from top to bottom. Use a header with a hamburger there (
hamburger-circle-revealormobile-fullscreen-menu). - Do not use it with a sticky bottom call-to-action bar (
cta-sticky-mobile-bar) on the same page. Pick one.
Reference behaviour
- First frame: the page is at the top. The bar is visible. Home has
aria-current="true"and the orange mark. - Scroll down more than 6px past 80px from the top: the bar slides down out of view over 220ms
cubic-bezier(.4,0,1,1). - Scroll up more than 6px: the bar slides back over 280ms
cubic-bezier(.16,1,.3,1). - Reach the end of the page (within 8px): the bar comes back, even if you were scrolling down, and Visit becomes current.
- Scroll position under 80px: the bar is always shown.
- While scrolling, the current item is the last section whose top is above 40% of the screen height. Its mark grows from
scaleX(0)toscaleX(1)over 160ms. - Tap an item: the page smooth-scrolls to that section. The current item updates as the section arrives.
- Tap Book: the page scrolls to “Today”, the class list. When Book is current, its label gets a 2px orange underline.
- Press Book: the orange square moves 3px right and 3px down and its shadow shrinks from 4px to 1px, like a key being pressed.
- Tab into the bar while it is hidden: it slides back so focus is never on something off screen.
- In the class list, “Book” buttons toggle to “Booked” (
aria-pressed). “Full” is disabled.
Structure
390 x 844 (54px status reserve on top, 84px browser bar drawn over the bottom)
+--------------------------------------+
| DEPOT[41] [] Open until 22:00 | 56px, 2px black rule under
| LIFT HEAVY. | 64px condensed 800
| COME BACK TOMORROW. |
| ...lede, 3 fact cells... |
| TODAY THU 8 OCT | sections, 2px rule on top
| 17:30 Barbell basics [BOOK] |
| +------+ |
| | [cal]| 60x60 raised 18px, 4px shadow
+=================| |=============+ 2px black top rule
| ▬ | 4px orange mark on current
| [home] [coach] +------+ [tag] [pin] | bar 64px, 5 equal columns
| HOME COACHES BOOK PRICES VISIT | 11px condensed caps
+--------------------------------------+ bottom: --chrome-bottom
| browser toolbar (not drawn here) |
+--------------------------------------+
<main class="page">is the whole site: header, hero, and four<section>s with idsbook,classes,prices,visit. The header has idhome.- The page has bottom padding of
bar 64 + lift 18 + chrome 84 + 24px, so the last line is never under the bar. <nav class="nav" aria-label="Site">isposition: fixed; left: 0; right: 0; bottom: var(--chrome-bottom), withpadding-bottom: env(safe-area-inset-bottom).- Inside:
<ul>as a 5-column grid,repeat(5, minmax(0, 1fr)), 64px tall. Each<li>holds one<a href="#section">with an SVG and a label. - The centre link has class
fab. Its icon sits in<span class="pad">, absolutely placed so its top is 18px above the bar. The label stays inside the bar at the bottom. - The current mark is the link’s
::before, 28×4px, sitting on the 2px top rule.
Content
- Logo “DEPOT” with “41” in a black block. Status “Open until 22:00” with an 8px orange square.
- Hero: “Lift heavy.” then “Come back tomorrow.” in grey. Lede: “A strength gym in an old tram depot. Barbells, coaching and 46 classes a week. No mirrors on the ceiling.” Facts: 18 racks, 46 classes a week, 06:00 first session.
- Today, Thu 8 Oct: 17:30 Barbell basics (Ines, 4 left), 18:15 Strongman circuit (Teo, booked), 19:00 Olympic lifting (Mara, full), 19:45 Conditioning 30 (Ines, 9 left), 20:30 Mobility and core (Ruy, 11 left).
- Coaches: Ines Alves, Teo Varga, Mara Kühn, Ruy Batista.
- Prices: Open gym €39/mo, Coached €69/mo (orange offset shadow), Ten pass €95.
- Visit: Rua do Depósito 41, Porto; week 06:00 to 22:00; weekend 08:00 to 18:00; 12 parking bays.
- Bar: Home, Coaches, Book, Prices, Visit.
Tokens
:root {
/* colour: concrete, black, safety orange */
--concrete: #ecebe7; /* page */
--surface: #f7f6f3; /* bar, cards */
--ink: #111111; /* text, rules, shadows */
--ink-2: #4f4d48; /* secondary text, resting labels */
--line: #c9c7c0; /* list hairlines, disabled border */
--orange: #ff5a00; /* Book fill, current mark, focus halo */
--on-orange: #111111; /* icon on orange */
/* type */
--cond: "Barlow Condensed", "Arial Narrow", sans-serif;
--sans: "Barlow", system-ui, sans-serif;
/* layout */
--top: 54px;
--chrome-bottom: 84px; /* browser toolbar in the Lounge frame; 0 in production */
--gutter: 20px;
--r: 2px; /* every radius */
--bar-h: 64px;
--fab: 60px;
--lift: 18px; /* how far Book rises above the bar */
/* motion */
--std: cubic-bezier(.2, .7, .2, 1);
--expo: cubic-bezier(.16, 1, .3, 1);
--t-bar: 280ms; /* show */
--t-micro: 160ms;
}
Typography
| Role | Family | Size / line | Weight | Tracking | Case |
|---|---|---|---|---|---|
| Bar label | Barlow Condensed | 11px / 1 | 700 | 0.1em | upper |
| Logo | Barlow Condensed | 26px / 1 | 800 | -0.01em | upper |
| Hero headline | Barlow Condensed | 64px / 0.86 | 800 | -0.01em | upper |
| Section heading | Barlow Condensed | 32px / 1 | 800 | 0 | upper |
| Section meta | Barlow Condensed | 13px | 700 | 0.08em | upper |
| Slot time | Barlow Condensed | 18px / 1 | 700 | 0 | digits |
| Slot button | Barlow Condensed | 14px / 1 | 700 | 0.08em | upper |
| Price | Barlow Condensed | 28px / 1 | 800 | 0 | digits |
| Body | Barlow | 16px / 1.5 | 400 | 0 | sentence |
| Small body | Barlow | 13px | 400 | 0 | sentence |
Labels are always visible. Do not ship an icon-only bar.
Motion
| Thing | Trigger | Property | From → to | Duration | Easing |
|---|---|---|---|---|---|
| Bar | scroll down > 6px, y > 80 | transform | 0 → translateY(100% + 18 + 84 + 8px) | 220ms | cubic-bezier(.4,0,1,1) |
| Bar | scroll up > 6px, end, top, focus | transform | hidden → 0 | 280ms | --expo |
| Current mark | section change | transform | scaleX(0) ↔ scaleX(1) | 160ms | --std |
| Book pad | press | transform, box-shadow | 0, 4px 4px → 3px 3px, 1px 1px | 160ms | --std |
| Slot button | toggle | background | none ↔ ink | 160ms | --std |
| Page | tap item | scroll | smooth scroll to section | browser | browser |
The hide distance includes the lift and the chrome, so the raised Book square goes fully off screen too.
Reduced motion: the bar never hides. Scroll is instant (scroll-behavior: auto). The mark, the pad press and the slot button change with no transition.
States
- Item resting: icon and label
--ink-2, no mark. - Item current: icon and label
--ink, 4px orange mark at full width (28px) on the top rule,aria-current="true". - Book resting: orange square, 2px black border, 2px radius, 4px black offset shadow, black 26px icon, label
--ink. - Book current: as resting, plus a 2px orange underline under the label, offset 4px.
- Book pressed: square moves 3px down-right, shadow 1px.
- Focus-visible on items: 2px black outline inset by 4px. On the Book square: 2px black outline offset 3px around the square.
- Focus-visible elsewhere: 2px black outline plus a 4px orange halo.
- Bar hidden: translated off screen, still in the DOM and in the tab order. Focus brings it back.
- Slot booked: black fill, surface text, “Booked”. Slot full:
--lineborder, grey text,disabled.
Accessibility
- The bar is
<nav aria-label="Site">with a list of links. These are links to places, so use<a href>, not buttons. - The current item uses
aria-current="true"because it marks a section of one page. On a site where each item is its own page, usearia-current="page". - Icons are
aria-hiddenSVGs. The visible label is the name. focusinon the nav removes the hidden state, so keyboard users never tab to an off-screen link.- Book is the same kind of link as the others. Do not give it a different role.
- Contrast:
#111111on#f7f6f3is about 18:1.#4f4d48on#f7f6f3is about 7.8:1.#111111on#ff5a00is about 7:1. Never put orange text on concrete; it is under 3:1. - Hit targets: each column is a fifth of the screen (70–76px) by 64px. The Book square is 60×60 and the full column below it is clickable.
- The header is not sticky. One fixed bar is enough on a phone.
Responsive rules
- At 360 wide, each column is 70px. Labels at 11px condensed caps fit. “Coaches” is the longest at 7 letters. Keep labels to one word, 8 letters or fewer.
- At 390 wide, columns are 76px.
- Always use
env(safe-area-inset-bottom)for bottom padding, andviewport-fit=coverin the viewport meta. - In the Lounge frame,
--chrome-bottomis 84px because the browser toolbar is drawn over the bottom of the page. In production set it to 0. Mobile browsers already place the page above their own toolbar. - At 768px and wider, hide this bar and show the same five links in a top header. Book becomes a filled button at the right of the header.
- If the site is installed as a PWA, keep the bar and stop hiding it on scroll. There is no browser toolbar fighting for space.
- Do not draw a status bar or a URL bar.
Acceptance checklist
Always
- Five items in equal columns. A product uses three to five, one per real destination.
- One raised centre action, the one task that matters most.
- Labels always show under icons.
- The current item follows the section in view and has one mark.
- The bar hides on scroll down, returns on scroll up, at the top and at the end of the page.
- Hiding moves the raised action off screen too.
- Bottom padding uses
env(safe-area-inset-bottom). - Page bottom padding keeps the last line clear of the bar.
- Tabbing into a hidden bar shows it.
- Every hit target is at least 44px.
- Reduced motion keeps the bar fixed and visible.
This demo
- Items read Home, Coaches, Book, Prices, Visit. Home is current on load.
- Book is a 60×60
#ff5a00square, 2px black border, 2px radius, 4px black offset shadow, raised 18px. - The bar is 64px,
#f7f6f3, with a 2px#111111top rule. - The current mark is 28×4px orange on the top rule.
- Labels are Barlow Condensed 700, 11px, 0.1em, uppercase.
Implementation notes
Hide and show with a small threshold, and always show at the top and the end. Throttle with requestAnimationFrame, not a timer.
let lastY = scrollY, ticking = false;
function onScroll() {
const y = scrollY;
const atEnd = innerHeight + y >= document.documentElement.scrollHeight - 8;
if (atEnd || y < 80 || y < lastY - 6) nav.classList.remove('hidden');
else if (y > lastY + 6) nav.classList.add('hidden');
if (Math.abs(y - lastY) > 6 || atEnd) lastY = y;
ticking = false;
}
addEventListener('scroll', () => {
if (!ticking) { ticking = true; requestAnimationFrame(onScroll); }
}, { passive: true });
nav.addEventListener('focusin', () => nav.classList.remove('hidden'));
Pick the current section in page order, not bar order. In this demo Book’s section comes before Coaches on the page, so looping over the bar items would pick the wrong one.
const sections = [...document.querySelectorAll('#home, main section')];
let current = 'home';
sections.forEach(s => { if (s.getBoundingClientRect().top < innerHeight * 0.4) current = s.id; });
if (atEnd) current = sections[sections.length - 1].id;
The raised square and the bar:
.nav { position: fixed; left: 0; right: 0; bottom: var(--chrome-bottom); padding-bottom: env(safe-area-inset-bottom, 0px); border-top: 2px solid var(--ink); transition: transform var(--t-bar) var(--expo); }
.nav.hidden { transform: translateY(calc(100% + var(--lift) + var(--chrome-bottom) + 8px)); transition: transform 220ms cubic-bezier(.4,0,1,1); }
.nav ul { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); height: var(--bar-h); }
.fab .pad { position: absolute; top: calc(var(--lift) * -1); left: 50%; width: var(--fab); height: var(--fab); margin-left: calc(var(--fab) / -2);
background: var(--orange); border: 2px solid var(--ink); border-radius: var(--r); box-shadow: 4px 4px 0 var(--ink); }
.fab:active .pad { transform: translate(3px, 3px); box-shadow: 1px 1px 0 var(--ink); }
Common mistakes:
- Using it on a marketing page. A one-visit page needs a header, not app chrome.
- Hiding on every pixel of scroll. Use the 6px threshold or it flickers.
- Not showing the bar at the end of the page, so the footer links are the only way out.
- Hiding the bar but leaving the raised square peeking above the bottom edge.
bottom: 0with noenv(safe-area-inset-bottom), so labels sit under the home indicator.- A sticky header and a bottom bar together. Two fixed bars eat the screen.
- A round floating button with a soft shadow. This family uses a 2px radius and a hard offset shadow.
- Orange text on the concrete page.
- Buttons instead of links for destinations.
Rebuild order:
- Build the page with real sections and ids.
- Add the fixed bar with five equal columns and labels.
- Raise the centre square with the hard shadow.
- Add the current mark and the scroll-linked current item.
- Add hide on scroll down and show on scroll up, top and end.
- Add
focusinto show, and the reduced-motion block. - Set
--chrome-bottomto 0 for production and check the safe-area padding.
Details
- Palette
- Type
- Barlow Condensed · Barlow
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
Tailwind CSS docsPut each instruction next to its code. Draw the grid with thin lines and let the empty sides carry a faint hatch.
All sourcesOn the shelves