Design Lounge
Install

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

Open full size
1280 × 800 · 18.7 KB · 218 lines

Use it

Free to use · credit appreciated

1 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.

Brief

217 lines

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)        └───────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
  • <section aria-labelledby> — CSS grid 392px 1fr, rows auto 1fr, column gap 48px.
  • .head spans both columns: grid 1fr 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>.
  • .ctrl absolutely positioned at right:64px; top:62px.
  • .tabs[role=tablist][aria-orientation=vertical] — four <button role=tab>; each is a 2-column grid 36px 1fr: mono number, <h3> title, .d collapsible description, .bar > i hairline at the bottom edge (starts at x=36px).
  • .big — absolute at left: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

RoleFamilySizeWeightLine-heightTrackingCase
HeadlineGeist60px5000.98−0.045emsentence
Giant numeralGeist200px5000.8−0.07emdigits, 1px outline only
EyebrowGeist Mono11px5001+0.14emUPPERCASE
Intro paragraphGeist15px4001.550sentence
Tab titleGeist17px50022px−0.015emsentence
Tab numberGeist Mono11px50022px0digits
Tab descriptionGeist13.5px4001.550sentence
Window headerGeist Mono12px50010sentence
ChipGeist Mono10.5px50010lowercase
Table headerGeist Mono10.5px5001.3+0.08emUPPERCASE
Stat valueGeist22px5001.2−0.03emdigits

Headline second line uses --ink-3. Selected tab number uses --accent.

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
.bar itab selectedtransform scaleX0 → 1 (origin left)6000mslineartime is linear by design; forwards
.bar isection hover / tab focus / pausedanimation-play-staterunning → paused——resumes from same point
.tab .dselectedgrid-template-rows0fr → 1fr420ms--ease-outprevious tab collapses on same clock
.tab h3hover / selectedcolor--ink-2 → --ink160ms--ease
.paneselectedopacity, translateY, scale, blur0, 10px, .985, 4px → 1, 0, 1, 0420msopacity/blur --ease, transform --ease-outoutgoing pane runs the reverse at the same time
.wirealways (pane 3)background-position0 → 8px900mslinear, infinite4px dash / 4px gap
.caretalways (pane 1)opacity1 → 01000mssteps(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 --line bottom border.
  • Tab hover: title --ink.
  • Tab selected: aria-selected="true", title --ink, number --accent, description expanded, hairline filling.
  • Tab focus-visible: 2px --accent outline, 4px offset, 4px radius.
  • Paused: class .paused on 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-3 background plus inset 2px 0 0 var(--accent).
  • Live node (pane 3): border rgba(134,225,176,.45) and a 4px --accent-dim ring.
  • Chip variants: neutral (--line-2 border, --ink-2), .g mint text + mint-dim fill, .y amber text, .r coral text.

Accessibility

  • Tabs follow the ARIA tabs pattern: role=tablist with aria-orientation="vertical", role=tab with aria-controls, role=tabpanel with aria-labelledby. Roving tabindex: 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> with aria-pressed and a label that reads “Pause autoplay” / “Resume autoplay”.
  • Decorative pieces (giant numeral, live cursors) are aria-hidden="true".
  • Contrast: --ink-2 on --bg 7.4:1; --ink-3 on --bg 3.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-rows 0fr → 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
Designed bySusan AcharyaKathmandu

Studied

Linear

Show the product doing the work. Fade it at the edges and keep one colour for what matters.

All sources