Design Lounge
Install

Nº 467 of 520

Vertical label service columns

Four tall columns with rotated service names. Hover, click or Enter widens one into a list of services with arrows; the others stay as labels.

Preview

Open full size
1280 × 800 · 14.4 KB · 216 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 "Vertical label service columns" (piece features-vertical-label-columns) 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

201 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

Studied from moremedia.at: the services block where each discipline is a tall column with its name set vertically, and pointing at one widens it into a list of what that discipline includes. This rebuild is for Quillon, a fictional brand and web studio, on a cool sage page with deep green ink instead of warm white. It is a horizontal accordion: one column is always open (2.6× the width of the others, a slightly darker fill), and the closed ones are rotated 40px labels with a mono number under them. The detail worth copying is that the closed state still reads as a strong typographic composition, so the section looks finished before anyone touches it.

Reference behaviour

  1. First frame: 76px header (tracked caps wordmark “QUILLON”, “EN”, “Menu” with a two-line icon). Eyebrow “SERVICES”, then “Four disciplines. / One desk.” at 50px.
  2. A 476px-tall box, 64px side margins, 1px border, 14px radius, split into four columns by 1px rules: Strategy 01, Identity 02, Build 03, Intelligence 04.
  3. Identity starts open: fill #D4DDD2, width ≈ 2.6× a closed column. Its panel shows “02 Identity” (38px), four service rows with ↗ arrows, a 15px description, and a mono footer “6–10 weeks · ends in a brand book” / “5 people on this desk”.
  4. Closed columns show the name rotated to read bottom-to-top, 40px Bricolage 700, bottom-aligned 30px from the floor, with the number 26px under it.
  5. Pointer enters a closed column (fine pointers only): after a 90ms intent delay it opens; the open one closes. Widths animate with flex-grow over 640ms expo out. Leaving the box keeps the last one open.
  6. Opening: the vertical label and number fade out (250ms); the panel fades in after a 200ms delay (380ms) and its title rises 10px.
  7. Click (or tap) a closed column: it opens immediately. Enter/Space on its button opens it and moves focus to its first service link.
  8. Hover a service row: text slides 6px right, arrow turns vermilion and nudges 2px up-right.
  9. Hover a closed column (before it opens): its label nudges 6px.
  10. Reduced motion: widths and fades switch instantly.

Structure

1280 × 800
┌────────────────────────────────────────────────────────────────────────┐
│ QUILLON                                              EN   Menu ═══     │ 76
│ SERVICES                                                               │
│ Four disciplines.                                       (50px)         │
│ One desk.                                                              │
│ ┌──────────┬─────────────────────────────┬──────────┬──────────┐       │
│ │          │ 02 Identity                 │          │          │       │
│ │          │ ─────────────────────────── │          │          │       │
│ │          │ Brand identity           ↗  │          │    I     │       │
│ │    S     │ Type & colour systems    ↗  │          │    n     │  476  │
│ │    t     │ Campaign concepts        ↗  │    B     │    t…    │       │
│ │    r…    │ Interface design         ↗  │    u…    │          │       │
│ │          │ A mark, a voice and …       │          │          │       │
│ │   01     │ 6–10 weeks · …   5 people…  │   03     │   04     │       │
│ └──────────┴─────────────────────────────┴──────────┴──────────┘       │
│  64                                                            64      │
└────────────────────────────────────────────────────────────────────────┘
  • header: wordmark link, .right with language tag and a button.menu.
  • section.intro: .eyebrow, h1.
  • div.cols (flex row). Each article.col holds:
    • button.tab[aria-expanded][aria-controls] filling the column, containing .vlabel and .num.
    • div.panel[role=region][aria-label] absolutely filling the column: h2 (with small number), ul of four links, p.desc, p.foot. Closed panels are inert.
  • Panels have min-width: 400px so text never reflows while the column is still growing; the column’s overflow clips it.

Tokens

:root {
  --page: #e4e9e2;    /* cool sage page */
  --open: #d4ddd2;    /* fill of the open column */
  --line: #bfcabd;    /* box border, column rules, row rules */
  --ink: #13201a;     /* deep green-black */
  --ink-2: #44524a;   /* eyebrow, description, footer */
  --ink-3: #57645c;   /* numbers, idle arrows */
  --accent: #d8432a;  /* arrow hover, focus ring */

  --sans: "Bricolage Grotesque", system-ui, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, monospace;

  --pad-x: 64px;
  --box-h: 476px;
  --radius: 14px;
  --grow-open: 2.6;
  --row-h: 54px;

  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --expo: cubic-bezier(0.16, 1, 0.3, 1);
  --t-grow: 640ms;
}

Typography

RoleFamilySizeWeightLine-heightTrackingNotes
WordmarkBricolage Grotesque, opsz 2422px70010.26emUPPER
Header linksBricolage Grotesque15px50010—
Lang / eyebrowJetBrains Mono11px50010.10 / 0.16emUPPER
H1Bricolage Grotesque, opsz 9650px7001.04−0.035emtwo lines
Vertical labelBricolage Grotesque, opsz 7240px7001−0.03emwriting-mode: vertical-rl + rotate 180°
Column numberJetBrains Mono13px40010vertical too
Panel titleBricolage Grotesque, opsz 7238px7001−0.03emnumber 12px mono before it
Service rowBricolage Grotesque18px6001−0.01em54px rows
DescriptionBricolage Grotesque15px4001.50max 40ch
FooterJetBrains Mono12px400 / 5001.50—

Motion

ThingTriggerPropertyFrom → ToDurationEasingDelayReduced motion
Columnopen/closeflex-grow1 ↔ 2.6640msexpo out—instant
Columnopen/closebackgroundpage ↔ --open300msstandard—instant
Label + numberopenopacity1 → 0250msstandard—instant
Panelopenopacity0 → 1380msstandard200msinstant
Panelcloseopacity1 → 0200msstandard—instant
Panel titleopentranslateY10px → 0500msexpo out200msnone
Closed labelhovertranslateY0 → 6px400msexpo out—none
Row texthover/focustranslateX0 → 6px250msexpo out—none
Row arrowhover/focustranslate, colour0 → (2px, −2px), --ink-3 → --accent250msexpo out—colour only

Hover-to-open waits 90ms so sweeping the pointer across the box does not open every column on the way.

States

  • Closed column: page background, vertical label and number visible, panel inert and invisible.
  • Closed column hover: label nudges 6px; after 90ms the column opens.
  • Open column: --open fill, aria-expanded="true", panel visible and interactive, its tab ignores pointer events.
  • Tab focus-visible: 2px accent ring inset 6px, 10px radius, drawn inside the column.
  • Row link focus-visible: 2px accent ring inset 2px; same slide and arrow treatment as hover.
  • Exactly one column is open at all times. There is no all-closed state.

Accessibility

  • Each column trigger is a real <button> with aria-expanded and aria-controls pointing at its panel. Its accessible name is the label plus number (“Strategy 01”).
  • Panels are role="region" with aria-label. Closed panels are inert, so their links are skipped by Tab and screen readers.
  • Keyboard: Tab moves across the four triggers, then into the open panel’s links. Enter/Space on a trigger opens it and focuses its first link.
  • Hover-to-open only runs on (hover: hover) and (pointer: fine). Touch uses tap.
  • Arrows are aria-hidden; the row text is the link name.
  • Contrast: #13201A on #E4E9E2 ≈ 14:1, #44524A on #D4DDD2 ≈ 6:1, #57645C on #E4E9E2 ≈ 5:1.
  • Trigger hit areas are the full column (≥ 200×476px). Rows are 54px tall.

Responsive rules

  • ≥ 1280: as specified.
  • 1024–1100: header, intro and box margins 32px. Vertical labels 32px.
  • 768–1023: same; “Intelligence” still fits vertically at 476px.
  • < 760: the box becomes a vertical accordion. Each closed column is a 64px row with “01 Strategy” set horizontally (24px). The open one hides its trigger row and shows the panel in flow (title 26px, rows 48px, footer stacked). Language tag hides.
  • At 375px: no horizontal scroll.

Acceptance checklist

Always

  • Four columns in a bordered, rounded box, separated by 1px rules.
  • Closed columns show the name rotated to read bottom-to-top, with the number under it, bottom-aligned.
  • Exactly one column is open on load and at all times.
  • Opening animates width (flex-grow), not by swapping layouts.
  • Panel content fades in after the width starts moving and does not reflow while growing.
  • Triggers are buttons with aria-expanded; closed panels are inert.
  • Enter/Space opens and moves focus to the first link.
  • Hover-to-open has an intent delay and is limited to fine pointers.
  • One accent colour, only on arrow hover and focus.
  • Below 760px it becomes a vertical accordion with no horizontal scroll.

This demo

  • Columns: Strategy 01, Identity 02, Build 03, Intelligence 04; Identity open at load.
  • H1 “Four disciplines. / One desk.” at 50px Bricolage Grotesque 700.
  • Open grow 2.6, 640ms expo out; box 476px tall, 14px radius.
  • Page #E4E9E2, open fill #D4DDD2, accent #D8432A.

Implementation notes

Vertical type that reads upward. writing-mode: vertical-rl alone reads top-to-bottom; rotate it 180° so it reads from the floor up, like a book spine on a shelf.

.vlabel { writing-mode: vertical-rl; transform: rotate(180deg);
  font: 700 40px/1 var(--sans); letter-spacing: -.03em; white-space: nowrap; }
.tab { position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: flex-end; gap: 26px; padding-bottom: 30px; }

Grow by flex, fade by delay. Animate flex-grow from 1 to 2.6. Keep the panel at a fixed min-width so its text does not rewrap every frame, and delay its fade so it appears into space that already exists.

.col { flex: 1 1 0; min-width: 0; transition: flex-grow 640ms var(--expo); }
.col.open { flex-grow: 2.6; background: var(--open); }
.panel { position: absolute; inset: 0; min-width: 400px; opacity: 0; visibility: hidden;
  transition: opacity .2s, visibility 0s .2s; }
.col.open .panel { opacity: 1; visibility: visible; transition: opacity .38s .2s, visibility 0s .2s; }

Single source of open state.

function open(col, focusPanel) {
  cols.forEach(c => {
    const on = c === col;
    c.classList.toggle('open', on);
    c.querySelector('.tab').setAttribute('aria-expanded', String(on));
    c.querySelector('.panel').inert = !on;
  });
  if (focusPanel) col.querySelector('.panel a').focus({ preventScroll: true });
}

Common mistakes: collapsing everything on mouseleave (the box flickers to four equal columns); animating width in px so the columns stop summing to the box; letting closed panels stay tabbable; rotating the label with transform: rotate(-90deg) on a horizontal element, which keeps its horizontal layout width and breaks the column; opening on hover for touch devices.

Details

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

Studied

Moremedia

Set the services as tall labels. Open one, and keep the rest as type.

All sources