№ 003
Leaf Nav
A paper-craft tab bar: a sage sheet stretches between tabs and its leading corner peels up.
- Version
- 1.0.0
- Download
- 10 KB · 3 files
- Added
- Oct 26, 2026
- Built with
- HTML, CSS, JS
- Needs
- Google Fonts
- Tags
- tab bar, paper craft, spring
Usage
Copy leaf-nav.css and leaf-nav.js (and assets/, if there is one) into your project, then:
Leaf — a paper-craft tab bar. The active tab is a sage sheet lying on top of overlapping cream tiles.
Its two edges are springs: the leading edge runs ahead, the trailing edge lags, so the sheet stretches
as it travels — and the leading corner peels up, showing the paper's underside.
new LeafNav(element, { tabs: [{ label, icon }], active: 0, onChange(tab, index) {} })
`icon` is a key of LeafNav.icons or a raw SVG string.Source
/* Leaf — paper-craft tab bar. The bar is an SVG that scales to its container;
icons and labels are real buttons laid over it. */
.lf {
position: relative;
width: 100%;
max-width: 760px;
container-type: inline-size;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: pan-y;
}
.lf::after {
content: "";
position: absolute;
left: 8%;
right: 8%;
bottom: -6%;
height: 14%;
z-index: -1;
border-radius: 50%;
background: rgba(110, 85, 50, 0.22);
filter: blur(18px);
}
.lf.is-dragging { cursor: grabbing; }
.lf-svg {
display: block;
width: 100%;
height: auto;
overflow: visible;
}
.lf-tile {
fill: url(#lf-cream);
stroke: rgba(255, 255, 255, 0.35);
stroke-width: 1;
}
.lf-grain { mix-blend-mode: multiply; opacity: 0.07; }
.lf-sheet-grain { mix-blend-mode: multiply; opacity: 0.22; }
.lf-tabs {
position: absolute;
left: 0;
width: 100%;
}
.lf-tab {
position: absolute;
top: 0;
height: 100%;
min-width: 44px;
min-height: 44px;
padding: 0;
border: 0;
border-radius: 999px;
background: none;
color: rgb(75, 68, 56);
font: inherit;
cursor: pointer;
}
.lf-tab:focus-visible {
outline: 2px solid #3f5a37;
outline-offset: -6px;
}
.lf-icon {
position: absolute;
left: 50%;
top: 20%;
display: grid;
width: clamp(22px, 5.6cqw, 40px);
aspect-ratio: 1;
transform: translateX(-50%);
}
.lf-icon svg { width: 100%; height: 100%; }
.lf-label {
position: absolute;
left: 50%;
top: 61%;
font-size: clamp(11px, 2.5cqw, 18px);
font-weight: 500;
letter-spacing: 0.005em;
white-space: nowrap;
transform: translate(-50%, calc(var(--k, 0) * -0.9cqw));
}
.lf-dot {
position: absolute;
left: 50%;
top: 84%;
width: clamp(4px, 1cqw, 7px);
aspect-ratio: 1;
border-radius: 50%;
background: currentColor;
opacity: var(--k, 0);
transform: translateX(-50%) scale(var(--k, 0));
} /* Leaf — a paper-craft tab bar. The active tab is a sage sheet lying on top of overlapping cream tiles.
* Its two edges are springs: the leading edge runs ahead, the trailing edge lags, so the sheet stretches
* as it travels — and the leading corner peels up, showing the paper's underside.
*
* new LeafNav(element, { tabs: [{ label, icon }], active: 0, onChange(tab, index) {} })
* `icon` is a key of LeafNav.icons or a raw SVG string.
*/
(function () {
// Geometry, in the SVG's own units (the bar scales to its container).
const W = 600;
const H = 96;
const LIFT_TOP = 12; // the sheet sits slightly proud of the bar: taller at the top…
const LIFT_BOTTOM = 2; // …and a touch lower at the bottom
const YT = -LIFT_TOP;
const YB = H + LIFT_BOTTOM;
const PAD_TOP = 24;
const PAD_BOTTOM = 30;
const CURL_BASE = 21; // resting curl width
const CURL_MAX = 16; // extra curl at full speed
const FOLD_STEPS = 22;
// Springs: the leading (right) edge is stiffer and quicker than the trailing (left) edge.
const SPRING_HI = { k: 300, c: 24 };
const SPRING_LO = { k: 180, c: 21 };
const SUBSTEP = 1 / 240;
const svg = (body) =>
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
const ICONS = {
home: svg('<path d="M3.5 11 12 3.5 20.5 11"/><path d="M5.5 9.8V20h5v-5.5h3V20h5V9.8"/>'),
user: svg('<circle cx="12" cy="8" r="4"/><path d="M4.5 21a7.5 7.5 0 0 1 15 0"/>'),
message: svg('<path d="M21 11.5a8.5 8.5 0 0 1-12.6 7.4L3 20l1.2-5A8.5 8.5 0 1 1 21 11.5Z"/>'),
camera: svg('<path d="M4 8h3l2-3h6l2 3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"/><circle cx="12" cy="13.5" r="3.5"/>'),
sliders: svg('<path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/>'),
};
const INK = [75, 68, 56]; // inactive icon + label
const PAPER = [255, 255, 255]; // icon + label on the green sheet
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
const mix = (a, b, t) => `rgb(${a.map((v, i) => Math.round(v + (b[i] - v) * t)).join(',')})`;
const cubic = (p0, p1, p2, p3, t) => {
const u = 1 - t;
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
};
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let uid = 0;
class LeafNav {
constructor(root, { tabs, active = 0, onChange } = {}) {
this.root = root;
this.tabs = tabs;
this.onChange = onChange;
this.n = tabs.length;
this.slot = W / this.n;
this.w = this.slot * 0.68; // horizontal run of each S-shaped tab edge
this.active = active;
this.running = false;
this.drag = null;
this.build();
this.restSide = () => (this.active === this.n - 1 ? 'L' : 'R');
const [lo, hi] = this.edges((active + 0.5) * this.slot);
this.lo = this.loT = lo;
this.hi = this.hiT = hi;
this.vlo = this.vhi = 0;
this.cR = this.cRT = this.restSide() === 'R' ? CURL_BASE : 0;
this.cL = this.cLT = this.restSide() === 'L' ? CURL_BASE : 0;
this.select(active, { move: false, force: true });
this.render();
}
icon(key) {
return ICONS[key] || key;
}
// Left/right edge positions of the sheet for a given centre. At the far ends the edge slides off-bar,
// so the sheet takes the bar's rounded cap instead of showing an S-curve.
edges(center) {
const half = this.slot / 2;
const u = (center - half) / half;
const v = (W - half - center) / half;
const pad = this.slot * 0.14; // a little extra paper so the curl never crowds the label
return [
center - half - this.w * clamp(1 - u, 0, 1) - pad * clamp(u, 0, 1),
center + half + this.w * clamp(1 - v, 0, 1) + pad * clamp(v, 0, 1),
];
}
// The S-shaped boundary between two tiles: horizontal at both ends, steep in the middle.
edgePath(c, y0, y1) {
return `C${c},${y0} ${c},${y1} ${c + this.w / 2},${y1}`;
}
build() {
const id = ++uid;
const { slot, w, n } = this;
this.root.classList.add('lf');
this.root.setAttribute('role', 'tablist');
this.root.setAttribute('aria-label', 'Main');
// cream tiles: each one's right edge overlaps the next; the first sits on top
let tiles = '';
for (let j = n - 2; j >= 0; j--) {
const c = (j + 1) * slot;
tiles += `<path class="lf-tile" filter="url(#lf-tile-${id})" d="M${-w},0 H${c - w / 2} ${this.edgePath(c, 0, H)} H${-w} Z"/>`;
}
const total = H + PAD_TOP + PAD_BOTTOM;
this.root.innerHTML = `
<svg class="lf-svg" viewBox="0 ${-PAD_TOP} ${W} ${total}" aria-hidden="true">
<defs>
<clipPath id="lf-pill-${id}"><rect x="0" y="0" width="${W}" height="${H}" rx="${H / 2}"/></clipPath>
<clipPath id="lf-sheet-${id}"><rect x="0" y="${YT}" width="${W}" height="${YB - YT}" rx="${(YB - YT) / 2}"/></clipPath>
<linearGradient id="lf-cream-${id}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f3eadf"/><stop offset="1" stop-color="#eee4d8"/>
</linearGradient>
<linearGradient id="lf-green-${id}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#757e63"/><stop offset="1" stop-color="#6a7358"/>
</linearGradient>
<linearGradient id="lf-under-r-${id}" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#d5c2aa"/><stop offset="0.55" stop-color="#e6d9c7"/><stop offset="1" stop-color="#f1e8dc"/>
</linearGradient>
<linearGradient id="lf-under-l-${id}" x1="1" y1="0" x2="0" y2="0">
<stop offset="0" stop-color="#d5c2aa"/><stop offset="0.55" stop-color="#e6d9c7"/><stop offset="1" stop-color="#f1e8dc"/>
</linearGradient>
<filter id="lf-bar-${id}" x="-10%" y="-40%" width="120%" height="200%">
<feDropShadow dx="0" dy="14" stdDeviation="14" flood-color="#6e5532" flood-opacity="0.2"/><feDropShadow dx="0" dy="2.5" stdDeviation="2.5" flood-color="#6e5532" flood-opacity="0.18"/>
</filter>
<filter id="lf-tile-${id}" filterUnits="userSpaceOnUse" x="-120" y="-40" width="${W + 240}" height="180">
<feDropShadow dx="6" dy="1" stdDeviation="6" flood-color="#6e5532" flood-opacity="0.3"/>
</filter>
<filter id="lf-sheet-${id}" x="-20%" y="-40%" width="140%" height="190%">
<feDropShadow dx="1.5" dy="4" stdDeviation="4" flood-color="#2c3a24" flood-opacity="0.26"/>
</filter>
<filter id="lf-blur-${id}" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2.6"/></filter>
<filter id="lf-grain-${id}" x="0" y="${-PAD_TOP}" width="${W}" height="${total}" filterUnits="userSpaceOnUse">
<feTurbulence type="fractalNoise" baseFrequency="1.1" numOctaves="2" seed="7" result="n"/>
<feColorMatrix in="n" type="matrix" values="0 0 0 0 0.30 0 0 0 0 0.24 0 0 0 0 0.16 1.5 0 0 0 -0.62"/>
</filter>
<filter id="lf-sheetgrain-${id}" x="0" y="${-PAD_TOP}" width="${W}" height="${total}" filterUnits="userSpaceOnUse">
<feTurbulence type="fractalNoise" baseFrequency="1.1" numOctaves="2" seed="7" result="n"/>
<feColorMatrix in="n" type="matrix" values="0 0 0 0 0.30 0 0 0 0 0.24 0 0 0 0 0.16 1.5 0 0 0 -0.62" result="g"/>
<feComposite in="g" in2="SourceGraphic" operator="in"/>
</filter>
</defs>
<!-- the card's thickness, then its face -->
<g filter="url(#lf-bar-${id})">
<rect x="0" y="3.5" width="${W}" height="${H}" rx="${H / 2}" fill="#e2d7c5"/>
</g>
<g clip-path="url(#lf-pill-${id})">
<rect x="0" y="0" width="${W}" height="${H}" fill="url(#lf-cream-${id})"/>
${tiles}
<rect class="lf-grain" x="0" y="${-PAD_TOP}" width="${W}" height="${total}" filter="url(#lf-grain-${id})"/>
</g>
<rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}" rx="${H / 2}" fill="none" stroke="#fff" stroke-opacity=".55"/>
<!-- the sheet -->
<g filter="url(#lf-sheet-${id})"><g clip-path="url(#lf-sheet-${id})">
<path class="lf-sheet-rim" fill="#eee5d6"/>
<path class="lf-sheet" fill="url(#lf-green-${id})"/>
<path class="lf-sheet-grain" filter="url(#lf-sheetgrain-${id})"/>
<path class="lf-fold-shadow-r" fill="none" stroke="#2a2d1c" stroke-opacity=".5" stroke-width="7" stroke-linecap="round" filter="url(#lf-blur-${id})"/>
<path class="lf-fold-shadow-l" fill="none" stroke="#2a2d1c" stroke-opacity=".5" stroke-width="7" stroke-linecap="round" filter="url(#lf-blur-${id})"/>
<path class="lf-under-r" fill="url(#lf-under-r-${id})"/>
<path class="lf-under-l" fill="url(#lf-under-l-${id})"/>
</g></g>
</svg>
<div class="lf-tabs"></div>`;
this.sheet = this.root.querySelector('.lf-sheet');
this.sheetRim = this.root.querySelector('.lf-sheet-rim');
this.sheetGrain = this.root.querySelector('.lf-sheet-grain');
this.under = { R: this.root.querySelector('.lf-under-r'), L: this.root.querySelector('.lf-under-l') };
this.foldShadow = { R: this.root.querySelector('.lf-fold-shadow-r'), L: this.root.querySelector('.lf-fold-shadow-l') };
this.svgEl = this.root.querySelector('.lf-svg');
const list = this.root.querySelector('.lf-tabs');
list.style.top = `${(PAD_TOP / total) * 100}%`;
list.style.height = `${(H / total) * 100}%`;
this.buttons = this.tabs.map((tab, i) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'lf-tab';
b.setAttribute('role', 'tab');
b.style.left = `${(i / n) * 100}%`;
b.style.width = `${100 / n}%`;
b.innerHTML = `<span class="lf-icon">${this.icon(tab.icon)}</span><span class="lf-label">${tab.label}</span><span class="lf-dot"></span>`;
b.addEventListener('click', () => {
if (this.suppressClick) return;
this.select(i);
});
list.appendChild(b);
return b;
});
this.root.addEventListener('keydown', (e) => {
const next = { ArrowRight: this.active + 1, ArrowLeft: this.active - 1, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
const i = (next + n) % n;
this.select(i);
this.buttons[i].focus();
});
this.root.addEventListener('pointerdown', (e) => this.dragStart(e));
this.root.addEventListener('pointermove', (e) => this.dragMove(e));
this.root.addEventListener('pointerup', (e) => this.dragEnd(e));
this.root.addEventListener('pointercancel', (e) => this.dragEnd(e));
}
// ---- selection ------------------------------------------------------------------------------
select(i, { move = true, force = false } = {}) {
if (i !== this.active || force) {
this.active = i;
this.buttons.forEach((b, j) => {
b.setAttribute('aria-selected', String(j === i));
if (j === i) b.setAttribute('aria-current', 'page'); else b.removeAttribute('aria-current');
b.tabIndex = j === i ? 0 : -1;
});
if (this.onChange) this.onChange(this.tabs[i], i);
}
if (move) this.moveTo((i + 0.5) * this.slot);
}
moveTo(center) {
[this.loT, this.hiT] = this.edges(center);
this.animate();
}
nearest(center) {
return clamp(Math.floor(center / this.slot), 0, this.n - 1);
}
// ---- dragging -------------------------------------------------------------------------------
toBar(e) {
const r = this.svgEl.getBoundingClientRect();
return ((e.clientX - r.left) / r.width) * W;
}
dragStart(e) {
if (!e.target.closest('.lf-tabs')) return;
this.drag = { x: e.clientX, moved: false };
}
dragMove(e) {
const d = this.drag;
if (!d) return;
if (!d.moved) {
if (Math.abs(e.clientX - d.x) < 6) return;
d.moved = true;
this.root.classList.add('is-dragging');
try { this.root.setPointerCapture(e.pointerId); } catch (_) { /* synthetic pointers can't be captured */ }
}
const center = clamp(this.toBar(e), this.slot / 2, W - this.slot / 2);
this.moveTo(center);
const i = this.nearest(center);
if (i !== this.active) this.select(i, { move: false });
}
dragEnd(e) {
const d = this.drag;
if (!d) return;
this.drag = null;
this.root.classList.remove('is-dragging');
if (d.moved) {
this.suppressClick = true;
setTimeout(() => { this.suppressClick = false; }, 0);
this.select(this.active); // settle on the nearest tab
}
if (this.root.hasPointerCapture && this.root.hasPointerCapture(e.pointerId)) {
this.root.releasePointerCapture(e.pointerId);
}
}
// ---- physics --------------------------------------------------------------------------------
animate() {
if (reducedMotion.matches) {
this.lo = this.loT; this.hi = this.hiT;
this.vlo = this.vhi = 0;
this.cR = this.cRT = this.restSide() === 'R' ? CURL_BASE : 0;
this.cL = this.cLT = this.restSide() === 'L' ? CURL_BASE : 0;
this.render();
return;
}
if (this.running) return;
this.running = true;
let last = performance.now();
const tick = (now) => {
let dt = Math.min((now - last) / 1000, 1 / 20);
last = now;
while (dt > 0) {
const h = Math.min(dt, SUBSTEP);
dt -= h;
this.vhi += (-SPRING_HI.k * (this.hi - this.hiT) - SPRING_HI.c * this.vhi) * h;
this.hi += this.vhi * h;
this.vlo += (-SPRING_LO.k * (this.lo - this.loT) - SPRING_LO.c * this.vlo) * h;
this.lo += this.vlo * h;
// The edge that leads gets the curl; at rest it sits on the inner side of the sheet.
const speed = (this.vhi + this.vlo) / 2;
const side = Math.abs(speed) > 80 ? (speed > 0 ? 'R' : 'L') : this.restSide();
const mag = CURL_BASE + clamp(Math.abs(speed) / 38, 0, CURL_MAX);
this.cRT = side === 'R' ? mag : 0;
this.cLT = side === 'L' ? mag : 0;
const k = 1 - Math.exp(-h * 14);
this.cR += (this.cRT - this.cR) * k;
this.cL += (this.cLT - this.cL) * k;
}
this.render();
const settled =
Math.abs(this.lo - this.loT) < 0.05 && Math.abs(this.hi - this.hiT) < 0.05 &&
Math.abs(this.vlo) < 0.5 && Math.abs(this.vhi) < 0.5 &&
Math.abs(this.cR - this.cRT) < 0.05 && Math.abs(this.cL - this.cLT) < 0.05;
if (settled) {
this.lo = this.loT; this.hi = this.hiT;
this.vlo = this.vhi = 0;
this.running = false;
this.render();
} else {
requestAnimationFrame(tick);
}
};
requestAnimationFrame(tick);
}
// ---- drawing --------------------------------------------------------------------------------
sheetPath() {
const { lo, hi, w } = this;
return [
`M${lo - w / 2},${YT} H${hi - w / 2}`,
this.edgePath(hi, YT, YB),
`H${lo + w / 2}`,
`C${lo},${YB} ${lo},${YT} ${lo - w / 2},${YT} Z`,
].join(' ');
}
// The lifted corner: the paper's underside, a crescent between the sheet's edge and a fold line
// that lies `c` units inside it — widest at the top, tapering to nothing at the bottom.
curl(edgeX, c, side) {
const { w } = this;
const x0 = edgeX - w / 2, x1 = edgeX, x2 = edgeX, x3 = edgeX + w / 2;
const dir = side === 'R' ? -1 : 1;
const fold = [];
for (let k = FOLD_STEPS; k >= 0; k--) {
const t = k / FOLD_STEPS;
const x = cubic(x0, x1, x2, x3, t) + dir * c * Math.pow(Math.sin(Math.PI * t), 1.6);
const y = cubic(YT, YT, YB, YB, t);
fold.push([x.toFixed(2), y.toFixed(2)]);
}
const outer = `M${x0},${YT} C${x1},${YT} ${x2},${YB} ${x3},${YB}`;
return {
under: `${outer} ${fold.map(([x, y]) => `L${x},${y}`).join(' ')} Z`,
// shadow line: the fold, nudged onto the green side
shadow: `M${fold.map(([x, y]) => `${(+x + dir * -3).toFixed(2)},${y}`).join(' L')}`,
};
}
render() {
const sheet = this.sheetPath();
this.sheet.setAttribute('d', sheet);
this.sheetRim.setAttribute('d', sheet);
this.sheetRim.setAttribute('transform', 'translate(-1.2 -0.2) scale(1)');
this.sheetGrain.setAttribute('d', sheet);
for (const [side, c, edge] of [['R', this.cR, this.hi], ['L', this.cL, this.lo]]) {
const visible = c > 0.4 && edge > -this.w && edge < W + this.w;
const { under, shadow } = visible ? this.curl(edge, c, side) : { under: '', shadow: '' };
this.under[side].setAttribute('d', under);
this.foldShadow[side].setAttribute('d', shadow);
}
// icons and labels turn to paper-white as the sheet covers them
const ramp = this.w * 0.6;
this.buttons.forEach((b, i) => {
const ci = (i + 0.5) * this.slot;
const k = clamp((ci - this.lo) / ramp, 0, 1) * clamp((this.hi - ci) / ramp, 0, 1);
b.style.color = mix(INK, PAPER, k);
b.style.setProperty('--k', k.toFixed(3));
});
}
}
LeafNav.icons = ICONS;
window.LeafNav = LeafNav;
})(); <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Leaf Nav</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="leaf-nav.css">
<style>
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
display: grid;
place-items: center;
padding: 24px 16px;
overflow: hidden;
background:
radial-gradient(ellipse 80% 60% at 50% 45%, #f8f2ea 0%, rgba(248, 242, 234, 0) 70%),
#f1e9df;
color: #3a3a2c;
font-family: "Figtree", system-ui, sans-serif;
}
/* soft leaf shadows, as if sunlight fell through a plant */
.leaves {
position: fixed;
top: -6vh;
left: -6vw;
width: 60vw;
height: 50vh;
pointer-events: none;
opacity: 0.1;
filter: blur(14px);
}
.leaves i {
position: absolute;
width: 38vw;
height: 7vh;
border-radius: 100% 0 100% 0;
background: #5a4a32;
transform-origin: 0 50%;
}
.stage {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
text-align: center;
}
.eyebrow {
color: #6d7a63;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.22em;
}
h1 {
margin: 12px 0 0;
font-size: clamp(48px, 11vw, 84px);
font-weight: 800;
letter-spacing: -0.04em;
line-height: 1;
}
.sub { margin: 12px 0 0; color: #66705d; font-size: 16px; }
.swap { animation: swap 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); }
@keyframes swap {
from { opacity: 0; transform: translateY(10px); }
}
#nav { margin-top: 56px; }
.hint { margin: 40px 0 0; color: #8a9180; font-size: 13px; }
.hint u { color: #3f4a38; text-decoration-color: rgba(63, 74, 56, 0.35); text-underline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .swap { animation: none; } }
</style>
</head>
<body>
<div class="leaves" aria-hidden="true">
<i style="top: 6vh; left: 2vw; transform: rotate(18deg)"></i>
<i style="top: 14vh; left: 0; transform: rotate(34deg); width: 30vw"></i>
<i style="top: 1vh; left: 8vw; transform: rotate(4deg); width: 26vw"></i>
<i style="top: 22vh; left: 4vw; transform: rotate(50deg); width: 22vw"></i>
</div>
<main class="stage">
<div class="eyebrow">LEAF</div>
<h1 id="title">Home</h1>
<p class="sub" id="sub">A quiet place to begin.</p>
<nav id="nav"></nav>
<p class="hint">Tap a tab — or <u>drag the sheet</u> along the bar.</p>
</main>
<script src="leaf-nav.js"></script>
<script>
const title = document.getElementById('title');
const sub = document.getElementById('sub');
const swap = (el, text) => {
el.textContent = text;
el.classList.remove('swap');
void el.offsetWidth;
el.classList.add('swap');
};
new LeafNav(document.getElementById('nav'), {
tabs: [
{ label: 'Home', icon: 'home', blurb: 'A quiet place to begin.' },
{ label: 'Profile', icon: 'user', blurb: 'You, folded neatly.' },
{ label: 'Chat', icon: 'message', blurb: 'Notes passed across the table.' },
{ label: 'Camera', icon: 'camera', blurb: 'Keep the good light.' },
{ label: 'Settings', icon: 'sliders', blurb: 'Adjust until it feels right.' },
],
onChange(tab) {
if (title.textContent !== tab.label) {
swap(title, tab.label);
swap(sub, tab.blurb);
}
},
});
</script>
</body>
</html>