Nº 153 of 520
Tabbed feature preview
Four vertical feature tabs with 6s autoplay progress hairlines; the preview pane crossfades between CSS-built product UIs, with a giant outlined step numeral.
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 "Tabbed feature preview" (piece features-tabbed-preview) 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º 153 · "Tabbed feature preview" · designlounge.vercel.app -->
# Tabbed feature preview
> **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 features section for a SaaS landing page (the fictional support platform "Northdesk"). A two-line 60px headline sits above a split: on the left, four vertical tabs; on the right, a large preview stage showing a product window built entirely from divs. The tabs autoplay: each active tab grows a 1px mint hairline under itself over 6 seconds, then hands off to the next. The preview crossfades with a 4px blur and a 10px rise, so changing tabs feels like focusing a lens, not swapping slides. A 200px outlined numeral ("01"…"04") in the bottom-left corner counts the active step. The detail worth copying: the progress is a CSS animation, and JS only listens for `animationend`, so pause-on-hover is one line of CSS.
## Reference behaviour
1. Initial state: tab 01 "Triage that reads first" is selected, its description is expanded, its hairline starts filling left → right. The stage shows the Inbox window with a highlighted ticket and a dashed "Suggested reply" box with a blinking 7×14px mint caret.
2. After 6000ms the hairline is full; the next tab becomes selected. Its description expands (grid-rows 0fr → 1fr, 420ms) while the previous collapses; the pane crossfades; the giant numeral changes to "02". After tab 04 it wraps to 01.
3. Hovering anywhere over the section pauses the hairline where it is (`animation-play-state: paused`). Moving the pointer out resumes from the same point.
4. Clicking a tab selects it immediately and restarts its hairline from 0.
5. Keyboard: when a tab has focus, autoplay is paused; Up/Left and Down/Right move selection (and focus) with wrap-around, Home/End jump to first/last.
6. A 32px square pause button top-right (label "Autoplay" / "Paused" in mono to its left) toggles autoplay. Its icon swaps between two bars and a play triangle; `aria-pressed` mirrors the state.
7. The four panes are: **Inbox** (5 ticket rows, sentiment chips, a suggested reply), **Shared view** (filter chips, a 5-row SLA table with health meters, two live-cursor name tags), **Automation rule** (When → If → Then nodes joined by marching dashed wires, plus a 4-cell run stats strip), **Audit log** (6 timestamped entries on a dotted timeline).
8. With reduced motion, autoplay is off from the start (button shows "Paused"), panes switch instantly, wires and caret stop.
## Structure
```
1280 × 800, padding 56 / 64
┌───────────────────────────────────────────────────────────────────────┐
│ ● NORTHDESK · PLATFORM Autoplay [❚❚] │
│ Every ticket arrives Northdesk reads, routes │
│ already half-solved. (60px, line 2 muted) … 340px paragraph │
│ ↕ 40px │
│ ┌ 392px tablist ──────┐ 48px ┌ stage (1fr, radius 14) ───────────┐ │
│ │01 Triage that reads │ │ ┌ window (panel-2, radius 10) ┐ │ │
│ │ description… │ │ │ 40px header · chips │ │ │
│ │ ━━━━━━━────────── │ │ │ rows / table / flow / log │ │ │
│ │02 Shared views, live│ │ └──────────────────────────────┘ │ │
│ │03 Automations… │ │ pane: inset 28px, v-centred │ │
│ │04 An audit trail… │ │ │ │
│ └─────────────────────┘ │ │ │
│ 01 /04 (200px outline) └───────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
… 177 more lines. Copy or download for the full brief. Loading the source…Brief
217 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 features section for a SaaS landing page (the fictional support platform “Northdesk”). A two-line 60px headline sits above a split: on the left, four vertical tabs; on the right, a large preview stage showing a product window built entirely from divs. The tabs autoplay: each active tab grows a 1px mint hairline under itself over 6 seconds, then hands off to the next. The preview crossfades with a 4px blur and a 10px rise, so changing tabs feels like focusing a lens, not swapping slides. A 200px outlined numeral (“01”…“04”) in the bottom-left corner counts the active step. The detail worth copying: the progress is a CSS animation, and JS only listens for animationend, so pause-on-hover is one line of CSS.
Reference behaviour
- Initial state: tab 01 “Triage that reads first” is selected, its description is expanded, its hairline starts filling left → right. The stage shows the Inbox window with a highlighted ticket and a dashed “Suggested reply” box with a blinking 7×14px mint caret.
- After 6000ms the hairline is full; the next tab becomes selected. Its description expands (grid-rows 0fr → 1fr, 420ms) while the previous collapses; the pane crossfades; the giant numeral changes to “02”. After tab 04 it wraps to 01.
- Hovering anywhere over the section pauses the hairline where it is (
animation-play-state: paused). Moving the pointer out resumes from the same point. - Clicking a tab selects it immediately and restarts its hairline from 0.
- Keyboard: when a tab has focus, autoplay is paused; Up/Left and Down/Right move selection (and focus) with wrap-around, Home/End jump to first/last.
- A 32px square pause button top-right (label “Autoplay” / “Paused” in mono to its left) toggles autoplay. Its icon swaps between two bars and a play triangle;
aria-pressedmirrors the state. - The four panes are: Inbox (5 ticket rows, sentiment chips, a suggested reply), Shared view (filter chips, a 5-row SLA table with health meters, two live-cursor name tags), Automation rule (When → If → Then nodes joined by marching dashed wires, plus a 4-cell run stats strip), Audit log (6 timestamped entries on a dotted timeline).
- With reduced motion, autoplay is off from the start (button shows “Paused”), panes switch instantly, wires and caret stop.
Structure
1280 × 800, padding 56 / 64
┌───────────────────────────────────────────────────────────────────────┐
│ ● NORTHDESK · PLATFORM Autoplay [❚❚] │
│ Every ticket arrives Northdesk reads, routes │
│ already half-solved. (60px, line 2 muted) … 340px paragraph │
│ ↕ 40px │
│ ┌ 392px tablist ──────┐ 48px ┌ stage (1fr, radius 14) ───────────┐ │
│ │01 Triage that reads │ │ ┌ window (panel-2, radius 10) ┐ │ │
│ │ description… │ │ │ 40px header · chips │ │ │
│ │ ━━━━━━━────────── │ │ │ rows / table / flow / log │ │ │
│ │02 Shared views, live│ │ └──────────────────────────────┘ │ │
│ │03 Automations… │ │ pane: inset 28px, v-centred │ │
│ │04 An audit trail… │ │ │ │
│ └─────────────────────┘ │ │ │
│ 01 /04 (200px outline) └───────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
<section aria-labelledby>— CSS grid392px 1fr, rowsauto 1fr, column gap 48px..headspans both columns: grid1fr 340px,align-items:end. Eyebrow (6px mint dot with 4px tinted ring + mono label),<h2>with a<span>for the muted second line, and a<p>..ctrlabsolutely positioned atright:64px; top:62px..tabs[role=tablist][aria-orientation=vertical]— four<button role=tab>; each is a 2-column grid36px 1fr: mono number,<h3>title,.dcollapsible description,.bar > ihairline at the bottom edge (starts at x=36px)..big— absolute atleft:58px; bottom:22px, the active number, with::after“/04”..stage— relative,overflow:hidden, contains four absolutely positioned.pane[role=tabpanel], each with one.win.
Tokens
:root {
/* surfaces — cool near-black, hairlines instead of shadows */
--bg: #0a0b0d;
--panel: #111317; /* stage base, flow nodes */
--panel-2: #16191e; /* product window */
--panel-3: #1c2026; /* highlighted row, inline code */
--line: #1f2329; /* hairlines */
--line-2: #2b3038; /* stronger hairline, chip border */
/* ink */
--ink: #eceef1;
--ink-2: #9aa1ac;
--ink-3: #646b76;
/* accent + status */
--accent: #86e1b0; /* mint: progress, selected number, positive chips */
--accent-dim: rgba(134,225,176,.12);
--warn: #f2c46d;
--red: #ef8b7a;
/* type */
--sans: "Geist", system-ui, sans-serif;
--mono: "Geist Mono", ui-monospace, monospace;
--fs-display: 60px;
--fs-numeral: 200px;
--fs-tab: 17px;
--fs-body: 14px;
--fs-small: 13px;
--fs-label: 11px;
/* space + shape */
--pad-y: 56px;
--pad-x: 64px;
--gap: 48px;
--r: 10px; /* window */
--r-lg: 14px; /* stage */
--r-chip: 5px;
--shadow-win: 0 24px 60px -24px rgba(0,0,0,.7);
/* motion */
--dwell: 6000ms;
--t-fast: 160ms;
--t-pane: 420ms;
--ease: cubic-bezier(.2,.7,.2,1);
--ease-out: cubic-bezier(.16,1,.3,1);
}
Stage background: radial-gradient(120% 80% at 80% 0%, #141a1a 0%, var(--panel) 55%), a barely visible green-tinted light from the top right.
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Headline | Geist | 60px | 500 | 0.98 | −0.045em | sentence |
| Giant numeral | Geist | 200px | 500 | 0.8 | −0.07em | digits, 1px outline only |
| Eyebrow | Geist Mono | 11px | 500 | 1 | +0.14em | UPPERCASE |
| Intro paragraph | Geist | 15px | 400 | 1.55 | 0 | sentence |
| Tab title | Geist | 17px | 500 | 22px | −0.015em | sentence |
| Tab number | Geist Mono | 11px | 500 | 22px | 0 | digits |
| Tab description | Geist | 13.5px | 400 | 1.55 | 0 | sentence |
| Window header | Geist Mono | 12px | 500 | 1 | 0 | sentence |
| Chip | Geist Mono | 10.5px | 500 | 1 | 0 | lowercase |
| Table header | Geist Mono | 10.5px | 500 | 1.3 | +0.08em | UPPERCASE |
| Stat value | Geist | 22px | 500 | 1.2 | −0.03em | digits |
Headline second line uses --ink-3. Selected tab number uses --accent.
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Notes |
|---|---|---|---|---|---|---|
.bar i | tab selected | transform scaleX | 0 → 1 (origin left) | 6000ms | linear | time is linear by design; forwards |
.bar i | section hover / tab focus / paused | animation-play-state | running → paused | — | — | resumes from same point |
.tab .d | selected | grid-template-rows | 0fr → 1fr | 420ms | --ease-out | previous tab collapses on same clock |
.tab h3 | hover / selected | color | --ink-2 → --ink | 160ms | --ease | |
.pane | selected | opacity, translateY, scale, blur | 0, 10px, .985, 4px → 1, 0, 1, 0 | 420ms | opacity/blur --ease, transform --ease-out | outgoing pane runs the reverse at the same time |
.wire | always (pane 3) | background-position | 0 → 8px | 900ms | linear, infinite | 4px dash / 4px gap |
.caret | always (pane 1) | opacity | 1 → 0 | 1000ms | steps(2) | blink |
Reduced motion: autoplay starts paused, bars render full (scaleX(1)), panes swap opacity in 1ms with no transform or blur, description expands instantly, wires and caret are static.
States
- Tab rest: title
--ink-2, number--ink-3, description collapsed, 1px--linebottom border. - Tab hover: title
--ink. - Tab selected:
aria-selected="true", title--ink, number--accent, description expanded, hairline filling. - Tab focus-visible: 2px
--accentoutline, 4px offset, 4px radius. - Paused: class
.pausedon the section (or:hover); hairline frozen. Control label “Paused”, icon play triangle. - Pause button hover: border
--ink-3, icon--ink. Focus-visible: 2px mint outline, 2px offset. - Highlighted row (pane 1):
--panel-3background plusinset 2px 0 0 var(--accent). - Live node (pane 3): border
rgba(134,225,176,.45)and a 4px--accent-dimring. - Chip variants: neutral (
--line-2border,--ink-2),.gmint text + mint-dim fill,.yamber text,.rcoral text.
Accessibility
- Tabs follow the ARIA tabs pattern:
role=tablistwitharia-orientation="vertical",role=tabwitharia-controls,role=tabpanelwitharia-labelledby. Rovingtabindex: selected tab 0, others −1. - Arrow keys (all four), Home and End move selection and focus. Selection follows focus.
- Autoplay must never move keyboard focus; it only changes
aria-selected. Any keyboard focus inside the tablist pauses autoplay (WCAG 2.2.2). - Pause control is a real
<button>witharia-pressedand a label that reads “Pause autoplay” / “Resume autoplay”. - Decorative pieces (giant numeral, live cursors) are
aria-hidden="true". - Contrast:
--ink-2on--bg7.4:1;--ink-3on--bg3.6:1 is used only for 11px mono labels and the muted headline line at 60px (large text).
Responsive rules
- ≥ 1280: as specified.
- 1024–1279: tab column 320px, padding 40px, headline 48px, the intro paragraph hides, header becomes one column.
- 768–1023: stack: headline, then tabs as a horizontal scroller of pills (title only), then the stage at 16:10. Giant numeral hides.
- < 640: the tabs become an accordion; each tab’s pane renders inside it below the description at full width, scaled to 0.8. Autoplay is off below 640.
Acceptance checklist
- Tab column is 392px; column gap 48px; stage radius 14px; panes inset 28px and vertically centred.
- Each selected tab’s hairline fills from x=36px to the right edge in exactly 6000ms, then the next tab activates; 04 wraps to 01.
- Hovering the section freezes the hairline mid-way and resumes from the same point on leave.
- Clicking a tab restarts its hairline from zero.
- The description expands via
grid-template-rows0fr → 1fr (420ms) with no height jump. - Pane crossfade includes 10px rise, 0.985 scale and 4px blur, 420ms.
- Giant 200px outlined numeral updates with the selection and shows “/04”.
- Arrow keys, Home and End work; roving tabindex is correct; focus never moves on autoplay.
- Pause button toggles
aria-pressed, icon and label. - Reduced motion: no autoplay on load, instant pane swap, static wires and caret.
- All four panes are made of HTML/CSS only, no images.
- No console errors.
Implementation notes
Let CSS own the clock. The progress bar is a CSS animation; JS advances on animationend. Restarting the animation needs a reflow:
function show(i) {
cur = (i + tabs.length) % tabs.length;
tabs.forEach((t, k) => {
t.setAttribute('aria-selected', k === cur);
t.tabIndex = k === cur ? 0 : -1;
const b = t.querySelector('.bar i');
b.style.animation = 'none'; void b.offsetWidth; b.style.animation = '';
});
panes.forEach((p, k) => p.classList.toggle('on', k === cur));
}
bar.addEventListener('animationend', () => { if (!paused && k === cur) show(cur + 1); });
Pause is CSS. No timers to clear:
.tab[aria-selected="true"] .bar i { animation: fill var(--dwell) linear forwards; }
section:hover .bar i, section.paused .bar i { animation-play-state: paused; }
@keyframes fill { to { transform: scaleX(1); } }
Height-animating the description without measuring:
.tab .d { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 420ms var(--ease-out); }
.tab .d > p { overflow: hidden; }
.tab[aria-selected="true"] .d { grid-template-rows: 1fr; }
Common mistakes: using setInterval for autoplay (drifts and can’t pause mid-way); moving focus when autoplay advances; stacking panes with display:none (kills the crossfade); letting the outgoing pane keep pointer-events, so clicks land on an invisible layer.
Details
- Palette
- Type
- Geist · Geist Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
LinearShow the product doing the work. Fade it at the edges and keep one colour for what matters.
All sourcesOn the shelves