Design Lounge
Install

Nº 148 of 520

Sticky comparison table

A 25-row plan comparison with a sticky plan header, numbered collapsible groups, check/dash/limit cells, tooltips and a differences-only switch.

Preview

Open full size
1280 × 800 · 14.9 KB · 160 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 "Sticky comparison table" (piece pricing-comparison-sticky) 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

204 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

The “compare every feature” block that lives under a pricing page, here for Gantry, a fictional CI/CD service. A 300px black control rail on the left carries a condensed 74px headline (“Every limit, in writing.”) and three controls; the right pane is a scrolling <table> whose plan header row (name, price, CTA) stays pinned while 25 feature rows in five numbered groups scroll underneath. Concrete-grey surfaces, 1px rules, mono values and one safety-orange accent make it read like a spec sheet stamped on sheet steel. The detail worth copying: a Differences only switch that hides every row where all four plans are equal, so the table collapses to the 21 rows that actually decide the purchase.

Reference behaviour

  1. Initial state: all five groups expanded, pane scrolled to top, header row at the top of the pane. Fleet (the recommended plan) has a 4px orange top bar, an orange PICK tag and a 7% orange tint down its whole column.
  2. Scroll the pane: the header row stays pinned at top:0. As soon as scrollTop > 4, the header gains a 2px orange underline plus a soft 18px shadow, so the pinned state is visible.
  3. Hover any cell or header cell: its entire column tints with rgba(18,18,18,.07); leaving the table clears it.
  4. Click a group header (“01 PIPELINES … 6 items ⌄”): its rows hide instantly and the chevron rotates −90° over 280ms. Click again: rows return, each fading in from −6px with a 24ms stagger by row index.
  5. Expand all / Collapse all in the rail apply the same to all five groups.
  6. Differences only is a square switch (role="switch"). On: the track fills orange, the knob slides 16px, and every row whose four values are identical (4 rows: Pipeline as code, Linux x64 runners, Secrets vault, Community forum) is removed. Off: they return.
  7. Hover or focus the circled i next to some feature names: a 230px ink tooltip with an orange 4px offset block shadow appears 10px to the right, sliding 4px over 140ms.
  8. Under the last group a footnote row explains air-gapped installs and the 15% annual discount.

Structure

1280 × 800
┌──────────── 300 ───────────┬───────────────────────── pane 980 (overflow:auto) ──────────────────┐
│ ⌂ GANTRY        CI/CD · v4 │ PLAN COMPARISON  │ HOBBY     │ TEAM      │▀FLEET PICK▀│ FOUNDRY   │ ← sticky
│                            │ BILLED MONTHLY   │ $0        │ $24       │ $96        │ Quote     │   180px
│ EVERY                      │ mono note        │ free…     │ per seat… │ per seat…  │ annual…   │
│ LIMIT,          74px       │                  │ [START →] │ [TRIAL →] │ [■TRIAL →] │ [TALK →]  │
│ IN              cond.      ├──────────────────┴───────────┴───────────┴────────────┴───────────┤
│ WRITING.  (orange)         │ 01 PIPELINES                                         6 items  ⌄   │ 48px
│                            ├──────────────────┬───────────┬───────────┬────────────┬───────────┤
│ mono sub 13px              │ Concurrent jobs ⓘ│ 2         │ 10        │ 40         │ Unlimited │ 44px
│                            │ Pipeline as code │ ✓         │ ✓         │ ✓          │ ✓         │
│ [Expand all][Collapse all] │ Matrix builds    │ —         │ ✓         │ ✓          │ ✓         │
│ [Differences only   ▢■]    │ …                                                                 │
│ legend ✓ — 25 GB           │ 02 COMPUTE …                                                      │
└────────────────────────────┴───────────────────────────────────────────────────────────────────┘
  feature column 340px · four plan columns share the remaining 640px (160px each)
  • <aside class="rail" aria-label="Comparison controls">: brand row, <h1>, mono sub, .ctrl (two buttons + switch) pinned to the bottom with margin-top:auto, .legend.
  • <div class="pane">: the only scroll container. Inside, one <table> with table-layout:fixed, border-collapse:separate (required for sticky borders), a <colgroup> setting the 340px feature column.
  • <thead>: one row of five <th scope="col">; every plan header cell carries data-c="1…4".
  • One <tbody data-g> per group: first a .g row (<td colspan="5"> holding a full-width <button aria-expanded>), then .f rows with <th scope="row"> feature name and four <td data-c> cells.
  • A final <tbody> with the footnote row (120px).
  • The rail background has a 61px vertical hairline grid (repeating-linear-gradient, 5% white).

Tokens

:root {
  /* colour: concrete, ink, one safety orange */
  --bg: #e4e3de;          /* pane + sticky header */
  --panel: #efeee9;       /* group header rows */
  --rail: #121212;        /* left rail */
  --rail-ink: #e9e7e1;    /* rail text */
  --rail-2: #8d8b84;      /* rail meta */
  --line: #c9c7c0;        /* cell rules */
  --line-2: #b3b1a9;      /* dash glyph, tooltip ring */
  --ink: #121212;         /* text, header underline, checks */
  --ink-2: #4a4944;
  --ink-3: #6e6c66;       /* mono meta */
  --accent: #ff5b14;      /* recommended bar, switch on, sticky underline */
  --accent-ink: #121212;  /* text on orange */
  --rec: rgba(255,91,20,.07);  /* recommended column tint */
  --hov: rgba(18,18,18,.07);   /* hovered column tint */

  /* type */
  --sans: "Archivo", Helvetica, Arial, sans-serif;   /* load wdth 62..125 */
  --mono: "JetBrains Mono", ui-monospace, monospace;

  /* layout */
  --rail-w: 300px;
  --feat-w: 340px;
  --head-h: 180px;
  --row-h: 44px;
  --group-h: 48px;

  /* motion */
  --t-micro: 140ms;
  --t-row: 280ms;
  --row-stagger: 24ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --expo: cubic-bezier(.16, 1, .3, 1);
}

Radii: none. Every corner is square, including the switch and the tooltip ring is the only circle (18px).

Typography

RoleFamilySizeWeightWidth (font-stretch)Line-heightTrackingCase
Rail headlineArchivo74px90062%0.86−0.01emUPPERCASE
Plan priceArchivo54px90062%1−0.01em—
Header leadArchivo30px90062%10UPPERCASE
Plan nameArchivo14px800125%1+0.06emUPPERCASE
Group titleArchivo15px800110%1+0.04emUPPERCASE
BrandArchivo15px800125%1+0.04emUPPERCASE
Feature nameArchivo14px400100%1.40sentence
Limit valueJetBrains Mono12.5px500—1.4−0.01emas data
Group numberJetBrains Mono12px700—1001–05
Labels / buttonsJetBrains Mono11px500–600—1+0.04–.1emUPPERCASE
TooltipJetBrains Mono12px400—1.450sentence

The contrast between ultra-condensed 62% display and extended 125% labels from the same variable family is the whole typographic idea.

Motion

ElementTriggerPropertyFrom → ToDurationEasingNotes
thead thscrollTop > 4box-shadownone → 0 2px 0 orange, 0 12px 18px -14px rgba(0,0,0,.35)200ms--easeclass on the pane
.g svg chevrongroup togglerotate0 ↔ −90°280ms--expo
tr.f revealgroup opensopacity, translateY0, −6px → 1, 0280ms--expodelay k × 24ms
switch knobDifferences toggletranslateX0 → 16px220ms--expotrack fills orange 140ms
.tip spanhover / focusopacity, translateX0, −4px → 1, 0140ms--ease
column tinthoverbackground-colortransparent → --hov140ms--ease
buttonshoverbackground, colouroutline → ink fill140ms--ease

Rows hide instantly on collapse (no height animation in a table; animating <tr> height is unreliable). Reduced motion: transitions 1ms, animations off, scroll-behavior:auto.

States

  • Pinned header: orange 2px underline + shadow while scrolled.
  • Recommended column: inset 0 4px 0 orange on its header, --rec tint on every cell, PICK tag (9px mono, orange fill), and its CTA pre-filled ink; on hover that CTA turns orange.
  • CTA hover (others): 1px ink outline → ink fill with --bg text.
  • Column hover: whole column --hov.
  • Group collapsed: aria-expanded="false", chevron points right, rows display:none.
  • Group header hover: #e8e6e0.
  • Switch on: aria-checked="true", orange track, knob in --rail colour.
  • Focus-visible: 2px orange outline, 2px offset, on every button including the i tooltips.
  • Cell kinds: included = 18px square-capped check, 2.4px stroke; not included = 12 × 2px --line-2 bar; limit = mono text.

Accessibility

  • A real <table> with a (visually hidden) <caption>, scope="col" on plan headers and scope="row" on feature names, so screen readers announce “Team, Concurrent jobs, 10”.
  • Check and dash cells carry role="img" and aria-label="Included" / "Not included".
  • Group toggles are <button aria-expanded> inside the row; Enter / Space toggles.
  • The differences control is <button role="switch" aria-checked>.
  • Tooltips: the trigger is a <button aria-label="About Concurrent jobs"> and the bubble has role="tooltip"; it shows on focus as well as hover. Escape is not needed because it hides on blur.
  • Contrast: --ink on --bg 15:1; --ink-3 on --bg 4.6:1 (meta only); --rail-2 on --rail 5.6:1.
  • Tab order: rail controls, then header CTAs, then group toggles and tooltip buttons in reading order.

Responsive rules

  • ≥ 1280: as specified.
  • 1024–1279: rail 240px, feature column 260px, headline 72px → keep 62% width.
  • 768–1023: rail becomes a 72px top bar (headline drops to 32px in one line, controls inline right); the table keeps a 220px feature column and the pane scrolls both ways; make the first column position:sticky; left:0 too.
  • < 640: swap the table for one plan at a time: a segmented plan picker pinned at top, rows show feature name + that plan’s value. Keep groups and the differences switch.

Acceptance checklist

  • Only the right pane scrolls; the rail never moves.
  • The plan header row is position:sticky; top:0 on each <th> and stays visible through all 25 rows.
  • Header gains the 2px orange underline only after the pane scrolls more than 4px.
  • Fleet column is tinted top to bottom and has the 4px orange top bar.
  • Each group header toggles its rows, rotates the chevron −90° and updates aria-expanded.
  • Re-opened rows fade in with a 24ms stagger.
  • Expand all / Collapse all affect all five groups.
  • Differences only hides exactly the rows where all four values match (4 of 25).
  • Hovering any cell tints its whole column.
  • Tooltips appear on hover and on keyboard focus, 10px right of the i, 230px wide.
  • Checks, dashes and limits are distinguishable without colour.
  • Display text uses Archivo at 62% width; labels at 125%.
  • No text wraps inside the 160px plan header cells at 1280 wide.
  • Reduced motion leaves all behaviour working with no animation.

Implementation notes

Sticky header cells, not a sticky <thead>. Put position:sticky on each th, give them an opaque background, and use separate borders, or the bottom rule scrolls away:

table { border-collapse: separate; border-spacing: 0; table-layout: fixed; width: 100%; }
thead th { position: sticky; top: 0; z-index: 5; height: 180px;
           background: var(--bg); border-bottom: 1px solid var(--ink); }
.stuck thead th { box-shadow: 0 2px 0 var(--accent), 0 12px 18px -14px rgba(0,0,0,.35); }

Column hover without per-cell listeners. Tag every cell with data-c and let one delegated listener write the column to the table:

table.addEventListener('mouseover', (e) => {
  const c = e.target.closest('[data-c]');
  table.dataset.hc = c ? c.dataset.c : '';
});
// css: table[data-hc="2"] [data-c="2"] { background-color: var(--hov); }

Differences only is a data flag, not a runtime comparison. When rendering, mark rows whose four values are identical with .same, then one class on the table hides them:

const same = values.every((v) => v === values[0]);
row.className = 'f' + (same ? ' same' : '');
// css: .diff tr.f.same { display: none; }

Common mistakes: making the whole page scroll so the sticky header is relative to the window and slides under the site nav; border-collapse: collapse (sticky borders vanish); tooltips clipped by overflow:hidden on the cell; forgetting the recommended tint on the sticky header cell, which reveals the rows scrolling beneath it.

Details

Palette
Type
Archivo · JetBrains Mono
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Abel

A catalogue. One product at a time.

All sources