25 CSS Sticky Navigation22 / 25
Mobile Bottom Tab Bar with Safe-Area Inset Padding
The thumb-zone navigation that mobile web apps and PWAs are judged on: five destinations pinned to the bottom of the scroll port, spaced above the home indicator instead of underneath it, with hit targets that survive a moving bus. Toggle the simulated inset in the demo to see exactly what breaks on a notched device.
Published
The code
<section class="sn-22" aria-label="Mobile bottom tab bar with safe area demo">
<div class="sn-22__stage">
<div class="sn-22__head">
<p class="sn-22__eyebrow">position: sticky · bottom: 0 · env()</p>
<h2 class="sn-22__title">Bottom tabs that clear the home indicator</h2>
<p class="sn-22__sub">Scroll the feed, switch tabs, then toggle the simulated device inset.</p>
<button class="sn-22__toggle" id="sn-22-toggle" type="button" aria-pressed="true">
<span class="sn-22__dotled" aria-hidden="true"></span>Simulated home indicator
</button>
</div>
<div class="sn-22__phone">
<div class="sn-22__screen">
<div class="sn-22__status" aria-hidden="true"><span>9:41</span><span class="sn-22__statusr"><span class="sn-22__sig"></span><span class="sn-22__bat"></span></span></div>
<div class="sn-22__scroll" id="sn-22-scroll" role="region" aria-label="Trail feed, scrollable" tabindex="0">
<header class="sn-22__top">
<h3 class="sn-22__apptitle" id="sn-22-title">Nearby trails</h3>
<p class="sn-22__appsub">Peak District · 12 routes</p>
</header>
<main class="sn-22__feed" id="sn-22-panel" aria-labelledby="sn-22-title">
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=800&auto=format&fit=crop" alt="Sunlight falling through a dense forest of tall trees" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Beech Hollow Loop</h4><p class="sn-22__cp">6.4 km · easy · 1h 40m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?q=80&w=800&auto=format&fit=crop" alt="A still lake reflecting mountains at dusk" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Still Water Circuit</h4><p class="sn-22__cp">11.2 km · moderate · 3h 05m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1519681393784-d120267933ba?q=80&w=800&auto=format&fit=crop" alt="A mountain range under a clear night sky" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Ridge Line Traverse</h4><p class="sn-22__cp">18.9 km · hard · 5h 20m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=800&auto=format&fit=crop" alt="Mist drifting between pine trees on a hillside" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Low Cloud Path</h4><p class="sn-22__cp">4.1 km · easy · 55m</p></div>
</article>
</main>
<nav class="sn-22__tabs" aria-label="Primary">
<button class="sn-22__tab" type="button" aria-current="page" data-label="Explore">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 11.5 12 4l9 7.5M6 10v9h12v-9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Explore</span>
</button>
<button class="sn-22__tab" type="button" data-label="Map">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 4 3 6.5v13L9 17l6 2.5 6-2.5v-13L15 6.5 9 4Zm0 0v13m6-10.5V20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Map</span>
</button>
<button class="sn-22__tab" type="button" data-label="Saved">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 4h10v16l-5-3.6L7 20V4Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Saved</span>
</button>
<button class="sn-22__tab" type="button" data-label="Inbox" aria-label="Inbox, 3 unread">
<span class="sn-22__badgewrap">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 6.5h18v11H3v-11Zm0 .5 9 6 9-6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__badge" aria-hidden="true">3</span>
</span>
<span class="sn-22__tlabel">Inbox</span>
</button>
<button class="sn-22__tab" type="button" data-label="Profile">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="sn-22__tlabel">Profile</span>
</button>
</nav>
</div>
<div class="sn-22__indicator" aria-hidden="true"></div>
</div>
</div>
<p class="sn-22__live" id="sn-22-live" role="status"></p>
</div>
</section><section class="sn-22" aria-label="Mobile bottom tab bar with safe area demo">
<div class="sn-22__stage">
<div class="sn-22__head">
<p class="sn-22__eyebrow">position: sticky · bottom: 0 · env()</p>
<h2 class="sn-22__title">Bottom tabs that clear the home indicator</h2>
<p class="sn-22__sub">Scroll the feed, switch tabs, then toggle the simulated device inset.</p>
<button class="sn-22__toggle" id="sn-22-toggle" type="button" aria-pressed="true">
<span class="sn-22__dotled" aria-hidden="true"></span>Simulated home indicator
</button>
</div>
<div class="sn-22__phone">
<div class="sn-22__screen">
<div class="sn-22__status" aria-hidden="true"><span>9:41</span><span class="sn-22__statusr"><span class="sn-22__sig"></span><span class="sn-22__bat"></span></span></div>
<div class="sn-22__scroll" id="sn-22-scroll" role="region" aria-label="Trail feed, scrollable" tabindex="0">
<header class="sn-22__top">
<h3 class="sn-22__apptitle" id="sn-22-title">Nearby trails</h3>
<p class="sn-22__appsub">Peak District · 12 routes</p>
</header>
<main class="sn-22__feed" id="sn-22-panel" aria-labelledby="sn-22-title">
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=800&auto=format&fit=crop" alt="Sunlight falling through a dense forest of tall trees" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Beech Hollow Loop</h4><p class="sn-22__cp">6.4 km · easy · 1h 40m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?q=80&w=800&auto=format&fit=crop" alt="A still lake reflecting mountains at dusk" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Still Water Circuit</h4><p class="sn-22__cp">11.2 km · moderate · 3h 05m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1519681393784-d120267933ba?q=80&w=800&auto=format&fit=crop" alt="A mountain range under a clear night sky" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Ridge Line Traverse</h4><p class="sn-22__cp">18.9 km · hard · 5h 20m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=800&auto=format&fit=crop" alt="Mist drifting between pine trees on a hillside" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Low Cloud Path</h4><p class="sn-22__cp">4.1 km · easy · 55m</p></div>
</article>
</main>
<nav class="sn-22__tabs" aria-label="Primary">
<button class="sn-22__tab" type="button" aria-current="page" data-label="Explore">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 11.5 12 4l9 7.5M6 10v9h12v-9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Explore</span>
</button>
<button class="sn-22__tab" type="button" data-label="Map">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 4 3 6.5v13L9 17l6 2.5 6-2.5v-13L15 6.5 9 4Zm0 0v13m6-10.5V20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Map</span>
</button>
<button class="sn-22__tab" type="button" data-label="Saved">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 4h10v16l-5-3.6L7 20V4Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Saved</span>
</button>
<button class="sn-22__tab" type="button" data-label="Inbox" aria-label="Inbox, 3 unread">
<span class="sn-22__badgewrap">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 6.5h18v11H3v-11Zm0 .5 9 6 9-6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__badge" aria-hidden="true">3</span>
</span>
<span class="sn-22__tlabel">Inbox</span>
</button>
<button class="sn-22__tab" type="button" data-label="Profile">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="sn-22__tlabel">Profile</span>
</button>
</nav>
</div>
<div class="sn-22__indicator" aria-hidden="true"></div>
</div>
</div>
<p class="sn-22__live" id="sn-22-live" role="status"></p>
</div>
</section>.sn-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--sn-22-bg);
--sn-22-bg: oklch(0.955 0.012 155);
--sn-22-surface: oklch(0.995 0.003 155);
--sn-22-ink: oklch(0.21 0.024 165);
--sn-22-mut: oklch(0.52 0.02 165);
--sn-22-acc: oklch(0.52 0.14 162);
--sn-22-line: oklch(0.21 0.024 165/.12);
--sn-22-safe: 26px;
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--sn-22-ink);
-webkit-font-smoothing: antialiased;
}
.sn-22[data-safe="off"] {
--sn-22-safe: 0px;
}
.sn-22 *,
.sn-22 *::before,
.sn-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sn-22__stage {
min-height: 100svh;
display: grid;
place-content: center;
justify-items: center;
gap: clamp(14px,2.4vw,22px);
padding: clamp(16px,4vw,48px);
background: radial-gradient(78% 50% at 50% 0%,oklch(0.98 0.03 160),transparent);
}
.sn-22__head {
display: grid;
justify-items: center;
gap: 8px;
text-align: center;
max-width: 48ch;
}
.sn-22__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--sn-22-acc);
}
.sn-22__title {
font-size: clamp(22px,3.3vw,34px);
line-height: 1.07;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.sn-22__sub {
font-size: 14.2px;
line-height: 1.58;
color: var(--sn-22-mut);
text-wrap: pretty;
}
.sn-22__toggle {
display: inline-flex;
align-items: center;
gap: 9px;
min-height: 42px;
margin-block-start: 4px;
padding: 0 16px;
border: 1px solid var(--sn-22-line);
border-radius: 99px;
background: var(--sn-22-surface);
font-family: inherit;
font-size: 13px;
font-weight: 620;
color: var(--sn-22-ink);
cursor: pointer;
transition: border-color .2s ease,background .2s ease;
}
.sn-22__toggle:hover {
border-color: color-mix(in oklch,var(--sn-22-acc) 45%,transparent);
}
.sn-22__toggle:focus-visible,
.sn-22__tab:focus-visible,
.sn-22__scroll:focus-visible {
outline: 2px solid var(--sn-22-acc);
outline-offset: 2px;
}
.sn-22__dotled {
inline-size: 9px;
block-size: 9px;
border-radius: 50%;
background: oklch(0.21 0.024 165/.22);
transition: background .2s ease,box-shadow .2s ease;
}
.sn-22__toggle[aria-pressed="true"] .sn-22__dotled {
background: var(--sn-22-acc);
box-shadow: 0 0 0 4px color-mix(in oklch,var(--sn-22-acc) 20%,transparent);
}
.sn-22__phone {
inline-size: min(90vw,330px);
block-size: min(70svh,640px);
padding: 11px;
border-radius: 44px;
background: linear-gradient(160deg,oklch(0.34 0.012 165),oklch(0.2 0.014 165));
box-shadow: 0 32px 70px -30px oklch(0.21 0.024 165/.6),inset 0 0 0 1.5px oklch(1 0 0/.14);
}
.sn-22__screen {
position: relative;
block-size: 100%;
border-radius: 34px;
overflow: hidden;
background: var(--sn-22-surface);
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.sn-22__status {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 20px 3px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: .02em;
}
.sn-22__statusr {
display: flex;
align-items: center;
gap: 5px;
}
.sn-22__sig {
inline-size: 16px;
block-size: 9px;
border-radius: 2px;
background: var(--sn-22-ink);
clip-path: polygon(0 60%,22% 60%,22% 100%,0 100%,0 60%,33% 30%,55% 30%,55% 100%,33% 100%,33% 30%,66% 0,100% 0,100% 100%,66% 100%);
}
.sn-22__bat {
inline-size: 20px;
block-size: 10px;
border-radius: 3px;
border: 1.4px solid var(--sn-22-ink);
position: relative;
}
.sn-22__bat::after {
content: '';
position: absolute;
inset: 1.6px;
inline-size: 64%;
border-radius: 1px;
background: var(--sn-22-ink);
}
.sn-22__scroll {
position: relative;
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
scroll-padding-block-end: calc(66px + var(--sn-22-safe));
display: grid;
grid-template-rows: auto 1fr auto;
align-content: start;
}
.sn-22__top {
padding: 10px 18px 12px;
display: grid;
gap: 2px;
}
.sn-22__apptitle {
font-size: 22px;
line-height: 1.1;
letter-spacing: -.03em;
font-weight: 730;
}
.sn-22__appsub {
font-size: 12.6px;
color: var(--sn-22-mut);
}
.sn-22__feed {
display: grid;
gap: 12px;
padding: 0 14px calc(18px + var(--sn-22-safe));
}
.sn-22__card {
border: 1px solid var(--sn-22-line);
border-radius: 16px;
overflow: hidden;
background: var(--sn-22-surface);
box-shadow: 0 10px 26px -22px oklch(0.21 0.024 165/.7);
}
.sn-22__img {
inline-size: 100%;
block-size: 118px;
object-fit: cover;
display: block;
background: linear-gradient(140deg,oklch(0.78 0.08 160),oklch(0.55 0.1 190));
}
.sn-22__cbody {
padding: 11px 13px 13px;
display: grid;
gap: 3px;
}
.sn-22__ch {
font-size: 14.6px;
line-height: 1.2;
letter-spacing: -.02em;
font-weight: 670;
}
.sn-22__cp {
font-size: 12.4px;
color: var(--sn-22-mut);
}
.sn-22__tabs {
position: sticky;
bottom: 0;
z-index: 10;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 2px;
padding: 6px 6px 0;
padding-block-end: max(6px,var(--sn-22-safe));
background: color-mix(in oklch,var(--sn-22-surface) 82%,transparent);
backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
border-top: 1px solid var(--sn-22-line);
transition: padding-block-end .32s cubic-bezier(.32,.72,.3,1);
}
.sn-22__tab {
display: grid;
justify-items: center;
align-content: center;
gap: 3px;
min-block-size: 48px;
padding: 6px 2px;
border: 0;
border-radius: 12px;
background: transparent;
font-family: inherit;
color: var(--sn-22-mut);
cursor: pointer;
transition: color .2s ease,background .2s ease;
}
.sn-22__tab svg {
inline-size: 21px;
block-size: 21px;
transition: transform .28s cubic-bezier(.34,1.56,.64,1);
}
.sn-22__tab:hover {
color: var(--sn-22-ink);
background: oklch(0.21 0.024 165/.05);
}
.sn-22__tab[aria-current="page"] {
color: var(--sn-22-acc);
}
.sn-22__tab[aria-current="page"] svg {
transform: translateY(-1px) scale(1.08);
}
.sn-22__tlabel {
font-size: 10.4px;
font-weight: 640;
letter-spacing: .01em;
}
.sn-22__badgewrap {
position: relative;
display: grid;
place-items: center;
}
.sn-22__badge {
position: absolute;
inset-block-start: -4px;
inset-inline-start: 11px;
min-inline-size: 16px;
block-size: 16px;
padding-inline: 4px;
display: grid;
place-items: center;
border-radius: 99px;
background: oklch(0.55 0.2 25);
color: oklch(1 0 0);
font-size: 10px;
font-weight: 700;
border: 1.5px solid var(--sn-22-surface);
}
.sn-22__indicator {
position: absolute;
inset-block-end: 8px;
inset-inline-start: 50%;
translate: -50% 0;
inline-size: 106px;
block-size: 5px;
border-radius: 99px;
background: oklch(0.21 0.024 165/.45);
opacity: 0;
transition: opacity .3s ease;
}
.sn-22[data-safe="on"] .sn-22__indicator {
opacity: 1;
}
.sn-22__live {
min-height: 20px;
font-size: 12.6px;
color: var(--sn-22-mut);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.sn-22__scroll {
scroll-behavior: auto;
}
.sn-22 * {
transition-duration: .01ms !important;
}
}.sn-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--sn-22-bg);
--sn-22-bg: oklch(0.955 0.012 155);
--sn-22-surface: oklch(0.995 0.003 155);
--sn-22-ink: oklch(0.21 0.024 165);
--sn-22-mut: oklch(0.52 0.02 165);
--sn-22-acc: oklch(0.52 0.14 162);
--sn-22-line: oklch(0.21 0.024 165/.12);
--sn-22-safe: 26px;
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--sn-22-ink);
-webkit-font-smoothing: antialiased;
}
.sn-22[data-safe="off"] {
--sn-22-safe: 0px;
}
.sn-22 *,
.sn-22 *::before,
.sn-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sn-22__stage {
min-height: 100svh;
display: grid;
place-content: center;
justify-items: center;
gap: clamp(14px,2.4vw,22px);
padding: clamp(16px,4vw,48px);
background: radial-gradient(78% 50% at 50% 0%,oklch(0.98 0.03 160),transparent);
}
.sn-22__head {
display: grid;
justify-items: center;
gap: 8px;
text-align: center;
max-width: 48ch;
}
.sn-22__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--sn-22-acc);
}
.sn-22__title {
font-size: clamp(22px,3.3vw,34px);
line-height: 1.07;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.sn-22__sub {
font-size: 14.2px;
line-height: 1.58;
color: var(--sn-22-mut);
text-wrap: pretty;
}
.sn-22__toggle {
display: inline-flex;
align-items: center;
gap: 9px;
min-height: 42px;
margin-block-start: 4px;
padding: 0 16px;
border: 1px solid var(--sn-22-line);
border-radius: 99px;
background: var(--sn-22-surface);
font-family: inherit;
font-size: 13px;
font-weight: 620;
color: var(--sn-22-ink);
cursor: pointer;
transition: border-color .2s ease,background .2s ease;
}
.sn-22__toggle:hover {
border-color: color-mix(in oklch,var(--sn-22-acc) 45%,transparent);
}
.sn-22__toggle:focus-visible,
.sn-22__tab:focus-visible,
.sn-22__scroll:focus-visible {
outline: 2px solid var(--sn-22-acc);
outline-offset: 2px;
}
.sn-22__dotled {
inline-size: 9px;
block-size: 9px;
border-radius: 50%;
background: oklch(0.21 0.024 165/.22);
transition: background .2s ease,box-shadow .2s ease;
}
.sn-22__toggle[aria-pressed="true"] .sn-22__dotled {
background: var(--sn-22-acc);
box-shadow: 0 0 0 4px color-mix(in oklch,var(--sn-22-acc) 20%,transparent);
}
.sn-22__phone {
inline-size: min(90vw,330px);
block-size: min(70svh,640px);
padding: 11px;
border-radius: 44px;
background: linear-gradient(160deg,oklch(0.34 0.012 165),oklch(0.2 0.014 165));
box-shadow: 0 32px 70px -30px oklch(0.21 0.024 165/.6),inset 0 0 0 1.5px oklch(1 0 0/.14);
}
.sn-22__screen {
position: relative;
block-size: 100%;
border-radius: 34px;
overflow: hidden;
background: var(--sn-22-surface);
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.sn-22__status {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 20px 3px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: .02em;
}
.sn-22__statusr {
display: flex;
align-items: center;
gap: 5px;
}
.sn-22__sig {
inline-size: 16px;
block-size: 9px;
border-radius: 2px;
background: var(--sn-22-ink);
clip-path: polygon(0 60%,22% 60%,22% 100%,0 100%,0 60%,33% 30%,55% 30%,55% 100%,33% 100%,33% 30%,66% 0,100% 0,100% 100%,66% 100%);
}
.sn-22__bat {
inline-size: 20px;
block-size: 10px;
border-radius: 3px;
border: 1.4px solid var(--sn-22-ink);
position: relative;
}
.sn-22__bat::after {
content: '';
position: absolute;
inset: 1.6px;
inline-size: 64%;
border-radius: 1px;
background: var(--sn-22-ink);
}
.sn-22__scroll {
position: relative;
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
scroll-padding-block-end: calc(66px + var(--sn-22-safe));
display: grid;
grid-template-rows: auto 1fr auto;
align-content: start;
}
.sn-22__top {
padding: 10px 18px 12px;
display: grid;
gap: 2px;
}
.sn-22__apptitle {
font-size: 22px;
line-height: 1.1;
letter-spacing: -.03em;
font-weight: 730;
}
.sn-22__appsub {
font-size: 12.6px;
color: var(--sn-22-mut);
}
.sn-22__feed {
display: grid;
gap: 12px;
padding: 0 14px calc(18px + var(--sn-22-safe));
}
.sn-22__card {
border: 1px solid var(--sn-22-line);
border-radius: 16px;
overflow: hidden;
background: var(--sn-22-surface);
box-shadow: 0 10px 26px -22px oklch(0.21 0.024 165/.7);
}
.sn-22__img {
inline-size: 100%;
block-size: 118px;
object-fit: cover;
display: block;
background: linear-gradient(140deg,oklch(0.78 0.08 160),oklch(0.55 0.1 190));
}
.sn-22__cbody {
padding: 11px 13px 13px;
display: grid;
gap: 3px;
}
.sn-22__ch {
font-size: 14.6px;
line-height: 1.2;
letter-spacing: -.02em;
font-weight: 670;
}
.sn-22__cp {
font-size: 12.4px;
color: var(--sn-22-mut);
}
.sn-22__tabs {
position: sticky;
bottom: 0;
z-index: 10;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 2px;
padding: 6px 6px 0;
padding-block-end: max(6px,var(--sn-22-safe));
background: color-mix(in oklch,var(--sn-22-surface) 82%,transparent);
backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
border-top: 1px solid var(--sn-22-line);
transition: padding-block-end .32s cubic-bezier(.32,.72,.3,1);
}
.sn-22__tab {
display: grid;
justify-items: center;
align-content: center;
gap: 3px;
min-block-size: 48px;
padding: 6px 2px;
border: 0;
border-radius: 12px;
background: transparent;
font-family: inherit;
color: var(--sn-22-mut);
cursor: pointer;
transition: color .2s ease,background .2s ease;
}
.sn-22__tab svg {
inline-size: 21px;
block-size: 21px;
transition: transform .28s cubic-bezier(.34,1.56,.64,1);
}
.sn-22__tab:hover {
color: var(--sn-22-ink);
background: oklch(0.21 0.024 165/.05);
}
.sn-22__tab[aria-current="page"] {
color: var(--sn-22-acc);
}
.sn-22__tab[aria-current="page"] svg {
transform: translateY(-1px) scale(1.08);
}
.sn-22__tlabel {
font-size: 10.4px;
font-weight: 640;
letter-spacing: .01em;
}
.sn-22__badgewrap {
position: relative;
display: grid;
place-items: center;
}
.sn-22__badge {
position: absolute;
inset-block-start: -4px;
inset-inline-start: 11px;
min-inline-size: 16px;
block-size: 16px;
padding-inline: 4px;
display: grid;
place-items: center;
border-radius: 99px;
background: oklch(0.55 0.2 25);
color: oklch(1 0 0);
font-size: 10px;
font-weight: 700;
border: 1.5px solid var(--sn-22-surface);
}
.sn-22__indicator {
position: absolute;
inset-block-end: 8px;
inset-inline-start: 50%;
translate: -50% 0;
inline-size: 106px;
block-size: 5px;
border-radius: 99px;
background: oklch(0.21 0.024 165/.45);
opacity: 0;
transition: opacity .3s ease;
}
.sn-22[data-safe="on"] .sn-22__indicator {
opacity: 1;
}
.sn-22__live {
min-height: 20px;
font-size: 12.6px;
color: var(--sn-22-mut);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.sn-22__scroll {
scroll-behavior: auto;
}
.sn-22 * {
transition-duration: .01ms !important;
}
}(() => {
const root = document.querySelector('.sn-22');
if (!root || root.dataset.snWired) return;
root.dataset.snWired = '1';
root.dataset.safe = 'on';
const tabs = [...root.querySelectorAll('.sn-22__tab')];
const title = root.querySelector('#sn-22-title');
const sub = root.querySelector('.sn-22__appsub');
const copy = { Explore: ['Nearby trails', 'Peak District · 12 routes'], Map: ['Live map', 'Following your position'], Saved: ['Saved routes', '5 offline · 2 shared'], Inbox: ['Inbox', '3 unread messages'], Profile: ['Ida Okonkwo', '148 km this month'] };
tabs.forEach((tab) => tab.addEventListener('click', () => {
tabs.forEach((t) => t === tab ? t.setAttribute('aria-current', 'page') : t.removeAttribute('aria-current'));
const [h, s] = copy[tab.dataset.label] || [];
if (h) { title.textContent = h; sub.textContent = s; }
root.querySelector('#sn-22-scroll').scrollTop = 0;
}));
const toggle = root.querySelector('#sn-22-toggle');
const live = root.querySelector('#sn-22-live');
toggle.addEventListener('click', () => {
const on = root.dataset.safe !== 'on';
root.dataset.safe = on ? 'on' : 'off';
toggle.setAttribute('aria-pressed', String(on));
live.textContent = on ? 'Inset active — padding-block-end resolves to 26px, clearing the home indicator.' : 'Inset removed — max() holds the 6px floor so the bar keeps its spacing.';
});
})();(() => {
const root = document.querySelector('.sn-22');
if (!root || root.dataset.snWired) return;
root.dataset.snWired = '1';
root.dataset.safe = 'on';
const tabs = [...root.querySelectorAll('.sn-22__tab')];
const title = root.querySelector('#sn-22-title');
const sub = root.querySelector('.sn-22__appsub');
const copy = { Explore: ['Nearby trails', 'Peak District · 12 routes'], Map: ['Live map', 'Following your position'], Saved: ['Saved routes', '5 offline · 2 shared'], Inbox: ['Inbox', '3 unread messages'], Profile: ['Ida Okonkwo', '148 km this month'] };
tabs.forEach((tab) => tab.addEventListener('click', () => {
tabs.forEach((t) => t === tab ? t.setAttribute('aria-current', 'page') : t.removeAttribute('aria-current'));
const [h, s] = copy[tab.dataset.label] || [];
if (h) { title.textContent = h; sub.textContent = s; }
root.querySelector('#sn-22-scroll').scrollTop = 0;
}));
const toggle = root.querySelector('#sn-22-toggle');
const live = root.querySelector('#sn-22-live');
toggle.addEventListener('click', () => {
const on = root.dataset.safe !== 'on';
root.dataset.safe = on ? 'on' : 'off';
toggle.setAttribute('aria-pressed', String(on));
live.textContent = on ? 'Inset active — padding-block-end resolves to 26px, clearing the home indicator.' : 'Inset removed — max() holds the 6px floor so the bar keeps its spacing.';
});
})();Here's a working CSS Sticky Navigation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Mobile Bottom Tab Bar with Safe-Area Inset Padding
Source: https://codefronts.com/navigation/css-sticky-navigation/mobile-bottom-tab-bar-with-safe-area-inset-padding/
The thumb-zone navigation that mobile web apps and PWAs are judged on: five destinations pinned to the bottom of the scroll port, spaced above the home indicator instead of underneath it, with hit targets that survive a moving bus. Toggle the simulated inset in the demo to see exactly what breaks on a notched device.
## HTML
```html
<section class="sn-22" aria-label="Mobile bottom tab bar with safe area demo">
<div class="sn-22__stage">
<div class="sn-22__head">
<p class="sn-22__eyebrow">position: sticky · bottom: 0 · env()</p>
<h2 class="sn-22__title">Bottom tabs that clear the home indicator</h2>
<p class="sn-22__sub">Scroll the feed, switch tabs, then toggle the simulated device inset.</p>
<button class="sn-22__toggle" id="sn-22-toggle" type="button" aria-pressed="true">
<span class="sn-22__dotled" aria-hidden="true"></span>Simulated home indicator
</button>
</div>
<div class="sn-22__phone">
<div class="sn-22__screen">
<div class="sn-22__status" aria-hidden="true"><span>9:41</span><span class="sn-22__statusr"><span class="sn-22__sig"></span><span class="sn-22__bat"></span></span></div>
<div class="sn-22__scroll" id="sn-22-scroll" role="region" aria-label="Trail feed, scrollable" tabindex="0">
<header class="sn-22__top">
<h3 class="sn-22__apptitle" id="sn-22-title">Nearby trails</h3>
<p class="sn-22__appsub">Peak District · 12 routes</p>
</header>
<main class="sn-22__feed" id="sn-22-panel" aria-labelledby="sn-22-title">
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=800&auto=format&fit=crop" alt="Sunlight falling through a dense forest of tall trees" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Beech Hollow Loop</h4><p class="sn-22__cp">6.4 km · easy · 1h 40m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?q=80&w=800&auto=format&fit=crop" alt="A still lake reflecting mountains at dusk" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Still Water Circuit</h4><p class="sn-22__cp">11.2 km · moderate · 3h 05m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1519681393784-d120267933ba?q=80&w=800&auto=format&fit=crop" alt="A mountain range under a clear night sky" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Ridge Line Traverse</h4><p class="sn-22__cp">18.9 km · hard · 5h 20m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=800&auto=format&fit=crop" alt="Mist drifting between pine trees on a hillside" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Low Cloud Path</h4><p class="sn-22__cp">4.1 km · easy · 55m</p></div>
</article>
</main>
<nav class="sn-22__tabs" aria-label="Primary">
<button class="sn-22__tab" type="button" aria-current="page" data-label="Explore">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 11.5 12 4l9 7.5M6 10v9h12v-9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Explore</span>
</button>
<button class="sn-22__tab" type="button" data-label="Map">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 4 3 6.5v13L9 17l6 2.5 6-2.5v-13L15 6.5 9 4Zm0 0v13m6-10.5V20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Map</span>
</button>
<button class="sn-22__tab" type="button" data-label="Saved">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 4h10v16l-5-3.6L7 20V4Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Saved</span>
</button>
<button class="sn-22__tab" type="button" data-label="Inbox" aria-label="Inbox, 3 unread">
<span class="sn-22__badgewrap">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 6.5h18v11H3v-11Zm0 .5 9 6 9-6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__badge" aria-hidden="true">3</span>
</span>
<span class="sn-22__tlabel">Inbox</span>
</button>
<button class="sn-22__tab" type="button" data-label="Profile">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="sn-22__tlabel">Profile</span>
</button>
</nav>
</div>
<div class="sn-22__indicator" aria-hidden="true"></div>
</div>
</div>
<p class="sn-22__live" id="sn-22-live" role="status"></p>
</div>
</section>
```
## CSS
```css
.sn-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--sn-22-bg);
--sn-22-bg: oklch(0.955 0.012 155);
--sn-22-surface: oklch(0.995 0.003 155);
--sn-22-ink: oklch(0.21 0.024 165);
--sn-22-mut: oklch(0.52 0.02 165);
--sn-22-acc: oklch(0.52 0.14 162);
--sn-22-line: oklch(0.21 0.024 165/.12);
--sn-22-safe: 26px;
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--sn-22-ink);
-webkit-font-smoothing: antialiased;
}
.sn-22[data-safe="off"] {
--sn-22-safe: 0px;
}
.sn-22 *,
.sn-22 *::before,
.sn-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sn-22__stage {
min-height: 100svh;
display: grid;
place-content: center;
justify-items: center;
gap: clamp(14px,2.4vw,22px);
padding: clamp(16px,4vw,48px);
background: radial-gradient(78% 50% at 50% 0%,oklch(0.98 0.03 160),transparent);
}
.sn-22__head {
display: grid;
justify-items: center;
gap: 8px;
text-align: center;
max-width: 48ch;
}
.sn-22__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--sn-22-acc);
}
.sn-22__title {
font-size: clamp(22px,3.3vw,34px);
line-height: 1.07;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.sn-22__sub {
font-size: 14.2px;
line-height: 1.58;
color: var(--sn-22-mut);
text-wrap: pretty;
}
.sn-22__toggle {
display: inline-flex;
align-items: center;
gap: 9px;
min-height: 42px;
margin-block-start: 4px;
padding: 0 16px;
border: 1px solid var(--sn-22-line);
border-radius: 99px;
background: var(--sn-22-surface);
font-family: inherit;
font-size: 13px;
font-weight: 620;
color: var(--sn-22-ink);
cursor: pointer;
transition: border-color .2s ease,background .2s ease;
}
.sn-22__toggle:hover {
border-color: color-mix(in oklch,var(--sn-22-acc) 45%,transparent);
}
.sn-22__toggle:focus-visible,
.sn-22__tab:focus-visible,
.sn-22__scroll:focus-visible {
outline: 2px solid var(--sn-22-acc);
outline-offset: 2px;
}
.sn-22__dotled {
inline-size: 9px;
block-size: 9px;
border-radius: 50%;
background: oklch(0.21 0.024 165/.22);
transition: background .2s ease,box-shadow .2s ease;
}
.sn-22__toggle[aria-pressed="true"] .sn-22__dotled {
background: var(--sn-22-acc);
box-shadow: 0 0 0 4px color-mix(in oklch,var(--sn-22-acc) 20%,transparent);
}
.sn-22__phone {
inline-size: min(90vw,330px);
block-size: min(70svh,640px);
padding: 11px;
border-radius: 44px;
background: linear-gradient(160deg,oklch(0.34 0.012 165),oklch(0.2 0.014 165));
box-shadow: 0 32px 70px -30px oklch(0.21 0.024 165/.6),inset 0 0 0 1.5px oklch(1 0 0/.14);
}
.sn-22__screen {
position: relative;
block-size: 100%;
border-radius: 34px;
overflow: hidden;
background: var(--sn-22-surface);
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.sn-22__status {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 20px 3px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: .02em;
}
.sn-22__statusr {
display: flex;
align-items: center;
gap: 5px;
}
.sn-22__sig {
inline-size: 16px;
block-size: 9px;
border-radius: 2px;
background: var(--sn-22-ink);
clip-path: polygon(0 60%,22% 60%,22% 100%,0 100%,0 60%,33% 30%,55% 30%,55% 100%,33% 100%,33% 30%,66% 0,100% 0,100% 100%,66% 100%);
}
.sn-22__bat {
inline-size: 20px;
block-size: 10px;
border-radius: 3px;
border: 1.4px solid var(--sn-22-ink);
position: relative;
}
.sn-22__bat::after {
content: '';
position: absolute;
inset: 1.6px;
inline-size: 64%;
border-radius: 1px;
background: var(--sn-22-ink);
}
.sn-22__scroll {
position: relative;
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
scroll-padding-block-end: calc(66px + var(--sn-22-safe));
display: grid;
grid-template-rows: auto 1fr auto;
align-content: start;
}
.sn-22__top {
padding: 10px 18px 12px;
display: grid;
gap: 2px;
}
.sn-22__apptitle {
font-size: 22px;
line-height: 1.1;
letter-spacing: -.03em;
font-weight: 730;
}
.sn-22__appsub {
font-size: 12.6px;
color: var(--sn-22-mut);
}
.sn-22__feed {
display: grid;
gap: 12px;
padding: 0 14px calc(18px + var(--sn-22-safe));
}
.sn-22__card {
border: 1px solid var(--sn-22-line);
border-radius: 16px;
overflow: hidden;
background: var(--sn-22-surface);
box-shadow: 0 10px 26px -22px oklch(0.21 0.024 165/.7);
}
.sn-22__img {
inline-size: 100%;
block-size: 118px;
object-fit: cover;
display: block;
background: linear-gradient(140deg,oklch(0.78 0.08 160),oklch(0.55 0.1 190));
}
.sn-22__cbody {
padding: 11px 13px 13px;
display: grid;
gap: 3px;
}
.sn-22__ch {
font-size: 14.6px;
line-height: 1.2;
letter-spacing: -.02em;
font-weight: 670;
}
.sn-22__cp {
font-size: 12.4px;
color: var(--sn-22-mut);
}
.sn-22__tabs {
position: sticky;
bottom: 0;
z-index: 10;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 2px;
padding: 6px 6px 0;
padding-block-end: max(6px,var(--sn-22-safe));
background: color-mix(in oklch,var(--sn-22-surface) 82%,transparent);
backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
border-top: 1px solid var(--sn-22-line);
transition: padding-block-end .32s cubic-bezier(.32,.72,.3,1);
}
.sn-22__tab {
display: grid;
justify-items: center;
align-content: center;
gap: 3px;
min-block-size: 48px;
padding: 6px 2px;
border: 0;
border-radius: 12px;
background: transparent;
font-family: inherit;
color: var(--sn-22-mut);
cursor: pointer;
transition: color .2s ease,background .2s ease;
}
.sn-22__tab svg {
inline-size: 21px;
block-size: 21px;
transition: transform .28s cubic-bezier(.34,1.56,.64,1);
}
.sn-22__tab:hover {
color: var(--sn-22-ink);
background: oklch(0.21 0.024 165/.05);
}
.sn-22__tab[aria-current="page"] {
color: var(--sn-22-acc);
}
.sn-22__tab[aria-current="page"] svg {
transform: translateY(-1px) scale(1.08);
}
.sn-22__tlabel {
font-size: 10.4px;
font-weight: 640;
letter-spacing: .01em;
}
.sn-22__badgewrap {
position: relative;
display: grid;
place-items: center;
}
.sn-22__badge {
position: absolute;
inset-block-start: -4px;
inset-inline-start: 11px;
min-inline-size: 16px;
block-size: 16px;
padding-inline: 4px;
display: grid;
place-items: center;
border-radius: 99px;
background: oklch(0.55 0.2 25);
color: oklch(1 0 0);
font-size: 10px;
font-weight: 700;
border: 1.5px solid var(--sn-22-surface);
}
.sn-22__indicator {
position: absolute;
inset-block-end: 8px;
inset-inline-start: 50%;
translate: -50% 0;
inline-size: 106px;
block-size: 5px;
border-radius: 99px;
background: oklch(0.21 0.024 165/.45);
opacity: 0;
transition: opacity .3s ease;
}
.sn-22[data-safe="on"] .sn-22__indicator {
opacity: 1;
}
.sn-22__live {
min-height: 20px;
font-size: 12.6px;
color: var(--sn-22-mut);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.sn-22__scroll {
scroll-behavior: auto;
}
.sn-22 * {
transition-duration: .01ms !important;
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.sn-22');
if (!root || root.dataset.snWired) return;
root.dataset.snWired = '1';
root.dataset.safe = 'on';
const tabs = [...root.querySelectorAll('.sn-22__tab')];
const title = root.querySelector('#sn-22-title');
const sub = root.querySelector('.sn-22__appsub');
const copy = { Explore: ['Nearby trails', 'Peak District · 12 routes'], Map: ['Live map', 'Following your position'], Saved: ['Saved routes', '5 offline · 2 shared'], Inbox: ['Inbox', '3 unread messages'], Profile: ['Ida Okonkwo', '148 km this month'] };
tabs.forEach((tab) => tab.addEventListener('click', () => {
tabs.forEach((t) => t === tab ? t.setAttribute('aria-current', 'page') : t.removeAttribute('aria-current'));
const [h, s] = copy[tab.dataset.label] || [];
if (h) { title.textContent = h; sub.textContent = s; }
root.querySelector('#sn-22-scroll').scrollTop = 0;
}));
const toggle = root.querySelector('#sn-22-toggle');
const live = root.querySelector('#sn-22-live');
toggle.addEventListener('click', () => {
const on = root.dataset.safe !== 'on';
root.dataset.safe = on ? 'on' : 'off';
toggle.setAttribute('aria-pressed', String(on));
live.textContent = on ? 'Inset active — padding-block-end resolves to 26px, clearing the home indicator.' : 'Inset removed — max() holds the 6px floor so the bar keeps its spacing.';
});
})();
```Here's a working CSS Sticky Navigation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Mobile Bottom Tab Bar with Safe-Area Inset Padding
Source: https://codefronts.com/navigation/css-sticky-navigation/mobile-bottom-tab-bar-with-safe-area-inset-padding/
The thumb-zone navigation that mobile web apps and PWAs are judged on: five destinations pinned to the bottom of the scroll port, spaced above the home indicator instead of underneath it, with hit targets that survive a moving bus. Toggle the simulated inset in the demo to see exactly what breaks on a notched device.
## HTML
```html
<section class="sn-22" aria-label="Mobile bottom tab bar with safe area demo">
<div class="sn-22__stage">
<div class="sn-22__head">
<p class="sn-22__eyebrow">position: sticky · bottom: 0 · env()</p>
<h2 class="sn-22__title">Bottom tabs that clear the home indicator</h2>
<p class="sn-22__sub">Scroll the feed, switch tabs, then toggle the simulated device inset.</p>
<button class="sn-22__toggle" id="sn-22-toggle" type="button" aria-pressed="true">
<span class="sn-22__dotled" aria-hidden="true"></span>Simulated home indicator
</button>
</div>
<div class="sn-22__phone">
<div class="sn-22__screen">
<div class="sn-22__status" aria-hidden="true"><span>9:41</span><span class="sn-22__statusr"><span class="sn-22__sig"></span><span class="sn-22__bat"></span></span></div>
<div class="sn-22__scroll" id="sn-22-scroll" role="region" aria-label="Trail feed, scrollable" tabindex="0">
<header class="sn-22__top">
<h3 class="sn-22__apptitle" id="sn-22-title">Nearby trails</h3>
<p class="sn-22__appsub">Peak District · 12 routes</p>
</header>
<main class="sn-22__feed" id="sn-22-panel" aria-labelledby="sn-22-title">
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=800&auto=format&fit=crop" alt="Sunlight falling through a dense forest of tall trees" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Beech Hollow Loop</h4><p class="sn-22__cp">6.4 km · easy · 1h 40m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?q=80&w=800&auto=format&fit=crop" alt="A still lake reflecting mountains at dusk" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Still Water Circuit</h4><p class="sn-22__cp">11.2 km · moderate · 3h 05m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1519681393784-d120267933ba?q=80&w=800&auto=format&fit=crop" alt="A mountain range under a clear night sky" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Ridge Line Traverse</h4><p class="sn-22__cp">18.9 km · hard · 5h 20m</p></div>
</article>
<article class="sn-22__card">
<img class="sn-22__img" src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=800&auto=format&fit=crop" alt="Mist drifting between pine trees on a hillside" width="800" height="500" loading="lazy" decoding="async">
<div class="sn-22__cbody"><h4 class="sn-22__ch">Low Cloud Path</h4><p class="sn-22__cp">4.1 km · easy · 55m</p></div>
</article>
</main>
<nav class="sn-22__tabs" aria-label="Primary">
<button class="sn-22__tab" type="button" aria-current="page" data-label="Explore">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 11.5 12 4l9 7.5M6 10v9h12v-9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Explore</span>
</button>
<button class="sn-22__tab" type="button" data-label="Map">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 4 3 6.5v13L9 17l6 2.5 6-2.5v-13L15 6.5 9 4Zm0 0v13m6-10.5V20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Map</span>
</button>
<button class="sn-22__tab" type="button" data-label="Saved">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 4h10v16l-5-3.6L7 20V4Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__tlabel">Saved</span>
</button>
<button class="sn-22__tab" type="button" data-label="Inbox" aria-label="Inbox, 3 unread">
<span class="sn-22__badgewrap">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 6.5h18v11H3v-11Zm0 .5 9 6 9-6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="sn-22__badge" aria-hidden="true">3</span>
</span>
<span class="sn-22__tlabel">Inbox</span>
</button>
<button class="sn-22__tab" type="button" data-label="Profile">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="sn-22__tlabel">Profile</span>
</button>
</nav>
</div>
<div class="sn-22__indicator" aria-hidden="true"></div>
</div>
</div>
<p class="sn-22__live" id="sn-22-live" role="status"></p>
</div>
</section>
```
## CSS
```css
.sn-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--sn-22-bg);
--sn-22-bg: oklch(0.955 0.012 155);
--sn-22-surface: oklch(0.995 0.003 155);
--sn-22-ink: oklch(0.21 0.024 165);
--sn-22-mut: oklch(0.52 0.02 165);
--sn-22-acc: oklch(0.52 0.14 162);
--sn-22-line: oklch(0.21 0.024 165/.12);
--sn-22-safe: 26px;
font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
color: var(--sn-22-ink);
-webkit-font-smoothing: antialiased;
}
.sn-22[data-safe="off"] {
--sn-22-safe: 0px;
}
.sn-22 *,
.sn-22 *::before,
.sn-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sn-22__stage {
min-height: 100svh;
display: grid;
place-content: center;
justify-items: center;
gap: clamp(14px,2.4vw,22px);
padding: clamp(16px,4vw,48px);
background: radial-gradient(78% 50% at 50% 0%,oklch(0.98 0.03 160),transparent);
}
.sn-22__head {
display: grid;
justify-items: center;
gap: 8px;
text-align: center;
max-width: 48ch;
}
.sn-22__eyebrow {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--sn-22-acc);
}
.sn-22__title {
font-size: clamp(22px,3.3vw,34px);
line-height: 1.07;
letter-spacing: -.03em;
font-weight: 700;
text-wrap: balance;
}
.sn-22__sub {
font-size: 14.2px;
line-height: 1.58;
color: var(--sn-22-mut);
text-wrap: pretty;
}
.sn-22__toggle {
display: inline-flex;
align-items: center;
gap: 9px;
min-height: 42px;
margin-block-start: 4px;
padding: 0 16px;
border: 1px solid var(--sn-22-line);
border-radius: 99px;
background: var(--sn-22-surface);
font-family: inherit;
font-size: 13px;
font-weight: 620;
color: var(--sn-22-ink);
cursor: pointer;
transition: border-color .2s ease,background .2s ease;
}
.sn-22__toggle:hover {
border-color: color-mix(in oklch,var(--sn-22-acc) 45%,transparent);
}
.sn-22__toggle:focus-visible,
.sn-22__tab:focus-visible,
.sn-22__scroll:focus-visible {
outline: 2px solid var(--sn-22-acc);
outline-offset: 2px;
}
.sn-22__dotled {
inline-size: 9px;
block-size: 9px;
border-radius: 50%;
background: oklch(0.21 0.024 165/.22);
transition: background .2s ease,box-shadow .2s ease;
}
.sn-22__toggle[aria-pressed="true"] .sn-22__dotled {
background: var(--sn-22-acc);
box-shadow: 0 0 0 4px color-mix(in oklch,var(--sn-22-acc) 20%,transparent);
}
.sn-22__phone {
inline-size: min(90vw,330px);
block-size: min(70svh,640px);
padding: 11px;
border-radius: 44px;
background: linear-gradient(160deg,oklch(0.34 0.012 165),oklch(0.2 0.014 165));
box-shadow: 0 32px 70px -30px oklch(0.21 0.024 165/.6),inset 0 0 0 1.5px oklch(1 0 0/.14);
}
.sn-22__screen {
position: relative;
block-size: 100%;
border-radius: 34px;
overflow: hidden;
background: var(--sn-22-surface);
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.sn-22__status {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 20px 3px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: .02em;
}
.sn-22__statusr {
display: flex;
align-items: center;
gap: 5px;
}
.sn-22__sig {
inline-size: 16px;
block-size: 9px;
border-radius: 2px;
background: var(--sn-22-ink);
clip-path: polygon(0 60%,22% 60%,22% 100%,0 100%,0 60%,33% 30%,55% 30%,55% 100%,33% 100%,33% 30%,66% 0,100% 0,100% 100%,66% 100%);
}
.sn-22__bat {
inline-size: 20px;
block-size: 10px;
border-radius: 3px;
border: 1.4px solid var(--sn-22-ink);
position: relative;
}
.sn-22__bat::after {
content: '';
position: absolute;
inset: 1.6px;
inline-size: 64%;
border-radius: 1px;
background: var(--sn-22-ink);
}
.sn-22__scroll {
position: relative;
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
scroll-padding-block-end: calc(66px + var(--sn-22-safe));
display: grid;
grid-template-rows: auto 1fr auto;
align-content: start;
}
.sn-22__top {
padding: 10px 18px 12px;
display: grid;
gap: 2px;
}
.sn-22__apptitle {
font-size: 22px;
line-height: 1.1;
letter-spacing: -.03em;
font-weight: 730;
}
.sn-22__appsub {
font-size: 12.6px;
color: var(--sn-22-mut);
}
.sn-22__feed {
display: grid;
gap: 12px;
padding: 0 14px calc(18px + var(--sn-22-safe));
}
.sn-22__card {
border: 1px solid var(--sn-22-line);
border-radius: 16px;
overflow: hidden;
background: var(--sn-22-surface);
box-shadow: 0 10px 26px -22px oklch(0.21 0.024 165/.7);
}
.sn-22__img {
inline-size: 100%;
block-size: 118px;
object-fit: cover;
display: block;
background: linear-gradient(140deg,oklch(0.78 0.08 160),oklch(0.55 0.1 190));
}
.sn-22__cbody {
padding: 11px 13px 13px;
display: grid;
gap: 3px;
}
.sn-22__ch {
font-size: 14.6px;
line-height: 1.2;
letter-spacing: -.02em;
font-weight: 670;
}
.sn-22__cp {
font-size: 12.4px;
color: var(--sn-22-mut);
}
.sn-22__tabs {
position: sticky;
bottom: 0;
z-index: 10;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: 2px;
padding: 6px 6px 0;
padding-block-end: max(6px,var(--sn-22-safe));
background: color-mix(in oklch,var(--sn-22-surface) 82%,transparent);
backdrop-filter: blur(20px) saturate(1.6);
-webkit-backdrop-filter: blur(20px) saturate(1.6);
border-top: 1px solid var(--sn-22-line);
transition: padding-block-end .32s cubic-bezier(.32,.72,.3,1);
}
.sn-22__tab {
display: grid;
justify-items: center;
align-content: center;
gap: 3px;
min-block-size: 48px;
padding: 6px 2px;
border: 0;
border-radius: 12px;
background: transparent;
font-family: inherit;
color: var(--sn-22-mut);
cursor: pointer;
transition: color .2s ease,background .2s ease;
}
.sn-22__tab svg {
inline-size: 21px;
block-size: 21px;
transition: transform .28s cubic-bezier(.34,1.56,.64,1);
}
.sn-22__tab:hover {
color: var(--sn-22-ink);
background: oklch(0.21 0.024 165/.05);
}
.sn-22__tab[aria-current="page"] {
color: var(--sn-22-acc);
}
.sn-22__tab[aria-current="page"] svg {
transform: translateY(-1px) scale(1.08);
}
.sn-22__tlabel {
font-size: 10.4px;
font-weight: 640;
letter-spacing: .01em;
}
.sn-22__badgewrap {
position: relative;
display: grid;
place-items: center;
}
.sn-22__badge {
position: absolute;
inset-block-start: -4px;
inset-inline-start: 11px;
min-inline-size: 16px;
block-size: 16px;
padding-inline: 4px;
display: grid;
place-items: center;
border-radius: 99px;
background: oklch(0.55 0.2 25);
color: oklch(1 0 0);
font-size: 10px;
font-weight: 700;
border: 1.5px solid var(--sn-22-surface);
}
.sn-22__indicator {
position: absolute;
inset-block-end: 8px;
inset-inline-start: 50%;
translate: -50% 0;
inline-size: 106px;
block-size: 5px;
border-radius: 99px;
background: oklch(0.21 0.024 165/.45);
opacity: 0;
transition: opacity .3s ease;
}
.sn-22[data-safe="on"] .sn-22__indicator {
opacity: 1;
}
.sn-22__live {
min-height: 20px;
font-size: 12.6px;
color: var(--sn-22-mut);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.sn-22__scroll {
scroll-behavior: auto;
}
.sn-22 * {
transition-duration: .01ms !important;
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.sn-22');
if (!root || root.dataset.snWired) return;
root.dataset.snWired = '1';
root.dataset.safe = 'on';
const tabs = [...root.querySelectorAll('.sn-22__tab')];
const title = root.querySelector('#sn-22-title');
const sub = root.querySelector('.sn-22__appsub');
const copy = { Explore: ['Nearby trails', 'Peak District · 12 routes'], Map: ['Live map', 'Following your position'], Saved: ['Saved routes', '5 offline · 2 shared'], Inbox: ['Inbox', '3 unread messages'], Profile: ['Ida Okonkwo', '148 km this month'] };
tabs.forEach((tab) => tab.addEventListener('click', () => {
tabs.forEach((t) => t === tab ? t.setAttribute('aria-current', 'page') : t.removeAttribute('aria-current'));
const [h, s] = copy[tab.dataset.label] || [];
if (h) { title.textContent = h; sub.textContent = s; }
root.querySelector('#sn-22-scroll').scrollTop = 0;
}));
const toggle = root.querySelector('#sn-22-toggle');
const live = root.querySelector('#sn-22-live');
toggle.addEventListener('click', () => {
const on = root.dataset.safe !== 'on';
root.dataset.safe = on ? 'on' : 'off';
toggle.setAttribute('aria-pressed', String(on));
live.textContent = on ? 'Inset active — padding-block-end resolves to 26px, clearing the home indicator.' : 'Inset removed — max() holds the 6px floor so the bar keeps its spacing.';
});
})();
```How this works
The bar itself is one line — position:sticky; bottom:0 inside the scrolling view. The part that separates a web app from a real one is the padding underneath it:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
.tabbar{
position: sticky;
bottom: 0;
padding-block-end: max(10px, env(safe-area-inset-bottom));
}env(safe-area-inset-bottom) resolves to the height of the home indicator on a gesture-navigation phone and to 0px everywhere else. Wrapping it in max() gives it a floor, so the bar keeps its normal breathing room on a laptop or an older handset and only grows where the hardware demands it. The inset returns zero unless the viewport meta carries viewport-fit=cover — the single most common reason developers conclude the feature is broken.
Height units matter just as much. 100vh is the large viewport on mobile: it assumes the browser's address bar is hidden, so a bottom bar sized against it sits below the fold until the user scrolls. 100dvh tracks the live viewport as the chrome collapses, and 100svh is the conservative small viewport. For a full-height app shell, dvh with a vh fallback is the pairing that behaves.
Everything else is ergonomics: every tab is at least 44×44px, labels stay visible instead of relying on icon literacy, the active destination is marked with aria-current="page", and the scroll port carries scroll-padding-block-end so the last card is never parked behind the bar.
Make it yours
- Icon-only bar: keep the labels in the DOM inside a visually hidden span — the accessible name must survive even when the text does not.
- Centre action button: give the middle tab a negative
margin-block-startand a circular background so it lifts above the bar, the compose/scan pattern. - Auto-hide on scroll down: reuse demo 07's directional logic and translate the bar by
calc(100% + env(safe-area-inset-bottom))so it clears the inset completely. - Blurred bar over content:
backdrop-filter:blur(20px)with a translucent background reads as native iOS; pair it with a hairline top border so the edge stays legible on white content. - Badge counts: cap the label at
9+and put the real number in the accessible name —aria-label="Inbox, 12 unread".
Gotchas — read before shipping
env(safe-area-inset-*)silently returns0pxwithoutviewport-fit=coverin the viewport meta tag.- Never use the inset alone as the padding value. On any device without a notch it collapses to zero and the bar loses all bottom spacing.
100vhon mobile is the large viewport, so full-height shells hide their bottom bar behind the browser UI until the page is scrolled. Use100dvhwith a100vhfallback.- A sticky bottom bar only pins while its container scrolls. If the content is shorter than the port, the bar sits at the end of the content — use
margin-block-start:autoin a flex column, orposition:fixed, when you need it anchored regardless. - Content hidden behind the bar is invisible to everyone but especially damaging on touch. Add
padding-block-endto the content andscroll-padding-block-endto the port, both at least the bar's height. - Tabs smaller than 44px, or spaced tighter than 8px, produce mistaps in the exact hand position this pattern exists to serve.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
env() safe-area insets have shipped since iOS 11 and Chrome 69; dvh/svh since Chrome 108, Safari 15.4 and Firefox 101. Firefox on Android exposes the insets as zero, which the max() floor handles gracefully.