Design Lounge
Install

Nº 157 of 520

Toggle switch set

Six toggle temperaments (classic, icon thumb, day/night, squish, labelled, segmented) in a smart-home preferences card with a live status index.

Preview

Open full size
1280 × 800 · 14.8 KB · 193 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 "Toggle switch set" (piece toggle-switch-set) 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

203 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 preferences card for a fictional smart-home app, Nook, where each of six rows uses a different toggle switch style: classic pill, Material-style icon thumb, day/night scene switch, squish-on-press, labelled ON/OFF, and a three-way segmented control. To the left, a 128px display headline (“Tune your home.”) sits over a mono index that reads back every switch’s live value. The feeling is warm, tactile and a little cheeky: peach paper, tomato accent, springy thumbs. The detail worth copying is that every switch is a real role="switch" button whose look is driven entirely by aria-checked, so state, styling and accessibility can never drift apart.

Reference behaviour

  1. Initial state: Away mode ON, Doorbell alerts ON, Evening scene NIGHT, Child lock OFF, Eco heating ON, Hallway motion AUTO. The index on the left mirrors these six values; ON-ish values are tomato, OFF/DAY are muted.
  2. Click (or Space/Enter on) any switch: aria-checked flips, the switch animates to its new state, the matching index row updates its value, and the card footer reads “Saved HH:MM · ” with the text flashing tomato for 900ms.
  3. 01 Classic (52×32): white 28px thumb slides 20px with a slight overshoot; track fills tomato.
  4. 02 Icon thumb (52×32, 2px outline): a 24px thumb shows an × when off (taupe thumb on outlined track) and a check when on (white thumb on a tomato track). Icons cross-fade with a 45° counter-rotation. While pressed, a 8px tomato halo (16% alpha) rings the thumb.
  5. 03 Day / night (76×36): day is a sky gradient with a cloud and a sun thumb with two soft halo rings; night cross-fades to a navy sky with four star dots while the thumb springs 40px right, turns bone and reveals three craters. The cloud sinks 20px and fades.
  6. 04 Squish (58×32): pressing stretches the thumb 10px toward its destination; releasing springs it across. Track goes ink when on.
  7. 05 Labelled (84×34, 9px radius, 1.5px ink border): off shows a dark 38×25 thumb on the left and “OFF” on the right; on fills the track tomato, the thumb turns white and slides 38px right, and “ON” appears on the left in white.
  8. 06 Segmented (Off / Auto / On, 3×64px): a white pill slides under the chosen option with an overshoot spring; choosing On turns the pill tomato with white text.
  9. Hovering a card row highlights the corresponding index row (text goes from --ink-2 to --ink).

Structure

1280 × 800, body padding 56 / 64, grid: 1fr | 620px, gap 56
┌───────────────────────────────────┬──────────────────────────────────────┐
│ ● NOOK · 14 ELMSTEAD ROAD         │ ┌──────────────────────────────────┐ │
│                                   │ │ Preferences       Ground floor…  │ │ 64
│ Tune                              │ ├──────────────────────────────────┤ │
│ your            (128px / .86)     │ │ [ic] Away mode 01 CLASSIC   (●=) │ │ 78
│ home.  ← tomato                   │ │ [ic] Doorbell  02 ICON      (=✓) │ │
│                                   │ │ [ic] Evening   03 DAY/NIGHT (☾ ) │ │
│ lede, 400px max                   │ │ [ic] Child lock 04 SQUISH   (● ) │ │
│ ──────────────────────────        │ │ [ic] Eco heat. 05 LABELLED  [ON] │ │
│ 01 Classic               ON       │ │ [ic] Hallway   06 SEGMENTED [|A|]│ │
│ … six rows, 1px hairlines         │ ├──────────────────────────────────┤ │
│ 06 Segmented           AUTO       │ │ ● All changes saved · 21:13      │ │
└───────────────────────────────────┴─└──────────────────────────────────┘─┘
  • <section class="intro">: eyebrow div, <h1> with <em> around “home.”, <p class="lede">, <ol class="index"> of six <li> each containing number, name and an <output>.
  • <section class="panel" aria-labelledby>: header (<h2> + mono meta), six .rows, footer with <output aria-live="polite">.
  • Each .row: 44px icon tile, .txt (title <b id> with a mono style tag <small>, description <p>), then the control.
  • Switches 01–05: <button class="sw …" role="switch" aria-checked aria-labelledby="lN"> with inner <span class="k"> thumb where needed.
  • Switch 06: <fieldset class="seg"> with a visually hidden <legend>, a decorative .pill span and three radio input+label pairs.

Tokens

:root {
  /* surfaces */
  --bg: #f7ede4;          /* peach paper page */
  --card: #fffbf7;        /* panel */
  --tile: #fbe2d4;        /* icon tiles */
  --off: #e6d6c9;         /* off track */
  --line: #ecdccf;        /* hairlines */
  --seg-bg: #f0e1d4;      /* segmented track */
  --m3-off: #f3e6db;      /* icon-thumb off track */
  /* ink */
  --ink: #2a1d17;
  --ink-2: #6b574b;
  --ink-3: #8c7466;
  /* accent */
  --accent: #f0552e;      /* tomato: on tracks, em, live values */
  --ok: #3fae6a;          /* saved dot */
  /* day / night */
  --sky: #8fd0f0;  --sky-2: #c9ecfb;
  --night: #1d2346; --night-top: #141a3a;
  --sun: #ffc23a;  --moon: #efe7d6; --crater: #d8ccb3;
  /* type */
  --font: "Bricolage Grotesque", system-ui, sans-serif;
  --mono: "DM Mono", ui-monospace, monospace;
  --fs-display: 128px; --fs-h2: 20px; --fs-row: 16px; --fs-body: 15px;
  --fs-desc: 13.5px; --fs-meta: 12px; --fs-tag: 10px;
  /* space & radius */
  --pad-page: 56px 64px; --pad-row: 16px 28px;
  --r-card: 24px; --r-tile: 12px;
  /* shadow */
  --shadow-card: 0 1px 0 var(--line), 0 30px 60px -30px rgba(120,60,30,.25);
  --shadow-thumb: 0 3px 8px rgba(42,29,23,.18), 0 1px 1px rgba(42,29,23,.1);
  /* motion */
  --t-fast: 160ms; --t: 240ms;
  --ease: cubic-bezier(.2,.7,.2,1);
  --spring: cubic-bezier(.34,1.56,.64,1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
Display h1Bricolage Grotesque (opsz 96)128px8000.86−0.055emsentence
Panel titleBricolage Grotesque20px7001.2−0.02emsentence
Row titleBricolage Grotesque16px6001.3−0.01emsentence
Row descriptionBricolage Grotesque13.5px4001.450sentence
LedeBricolage Grotesque16px4001.450sentence
EyebrowDM Mono12px5001+0.14emUPPERCASE
Style tagDM Mono10px5001+0.10emUPPERCASE
Index rowsDM Mono12px400/50010 / +0.08em valuessentence / UPPERCASE
Switch labelsDM Mono11–12px5001+0.10emON/OFF caps, segment sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasing
Classic thumbtoggletranslateX0 → 20px240ms--spring
All trackstogglebackground, border colouroff → on colour240ms--ease
Icon thumbtoggletranslateX, background0 → 20px, taupe → white240ms--ease
Icon thumb glyphstoggleopacity, scale, rotate1,1,0 ↔ 0,.4,±45°160 / 240ms--ease
Day/night thumbtoggletranslateX, bg, halo0 → 40px, sun → moon420ms--spring
Night sky layertoggleopacity0 → 1240ms--ease
CloudtoggletranslateY, opacity0,1 → 20px,0240 / 160ms--ease
Squish thumbpress / toggleleft, right insetsright 30 → 20 on press; swap ends380ms--spring
Labelled thumbtoggletranslateX, background0 → 38px, ink → white240ms--ease
Segment pillchangetranslateXn × 64px320ms--spring
Footer statusany changecolour keyframetomato → --ink-3900ms--ease

Reduced motion: all transitions to 1ms and the flash keyframe removed. Every state is still drawn.

States

  • Off / on: driven only by [aria-checked] (switches) or :checked (segments). Never toggle a separate class.
  • Hover (row): linked index row ink darkens. Switches have cursor:pointer, no hover colour change (the motion is the reward).
  • Active: squish thumb stretches; icon thumb gets an 8px halo.
  • Focus-visible: 2px --ink outline, 3px offset, on the switch button or the segment label after a focused radio.
  • Saved: footer dot green #3fae6a, text updates and flashes.

Accessibility

  • Switches are <button role="switch"> with aria-checked="true|false" and aria-labelledby pointing at the row title. Space and Enter toggle natively via button semantics.
  • The day/night switch reads as on/off; its state name (NIGHT/DAY) is shown in the index and footer for sighted users and announced via the live footer.
  • Segmented control is a native radio group inside a <fieldset>; arrow keys move between options; the legend is visually hidden but present.
  • Footer <output aria-live="polite"> announces each save.
  • Labelled switch text is decorative duplication; hide inactive text with color: transparent, not display:none, so width stays stable.
  • Contrast: --ink-2 on --card is 6.6:1; white on --accent is 3.4:1 and only used for 11px caps inside large controls; keep the row title as the accessible name.
  • Hit targets: switches are 32–36px tall inside 78px rows; extend with padding if the design system requires 44px.

Responsive rules

  • ≥ 1280: two columns as drawn.
  • 1024–1279: right column stays 620px; display size scales to 112px.
  • < 1100 (implemented breakpoint): single column, display 96px, the index list hides (the card already shows every state), body scrolls.
  • < 640: rows wrap description under the title; switches stay right-aligned at fixed size; the segmented control drops to 3×56px.

Acceptance checklist

  • Six rows, each with a visibly different switch; all six initial states match the list above.
  • Every boolean switch is a <button role="switch"> whose visuals key off aria-checked only.
  • Classic thumb travel is exactly 20px; day/night 40px; labelled 38px; segment pill 64px per step.
  • Spring easing cubic-bezier(.34,1.56,.64,1) is used on thumbs that slide; colour fades use cubic-bezier(.2,.7,.2,1).
  • Icon thumb shows × when off and ✓ when on, cross-fading with rotation.
  • Day/night swaps sky, cloud, sun-to-moon and craters together.
  • Squish thumb stretches 10px toward its destination while pressed.
  • Index values on the left update on every change (ON/OFF, DAY/NIGHT, OFF/AUTO/ON).
  • Footer announces “Saved HH:MM · ” through a polite live region.
  • Focus ring visible on every switch and segment.
  • Reduced motion removes animation but keeps every state.
  • No borders lost on the outlined switches (check reset specificity).

Implementation notes

Reset buttons with zero specificity. A button.sw { border: 0 } reset outranks .m3 { border: 2px … } and silently erases the outlined tracks. Wrap the reset in :where():

:where(button.sw) { appearance: none; border: 0; padding: 0; background: none; cursor: pointer; position: relative; }
.m3 { width: 52px; height: 32px; border-radius: 16px; border: 2px solid var(--ink-3); }
.m3[aria-checked=true] { background: var(--accent); border-color: var(--accent); }

Squish by insets, not scale. Scaling the thumb turns a circle into an egg. Pin it with left/right and let :active move only the trailing edge:

.sq .k { position: absolute; top: 4px; bottom: 4px; left: 4px; right: 30px; border-radius: 12px;
         transition: left 380ms var(--spring), right 380ms var(--spring); }
.sq:active .k { right: 20px; }
.sq[aria-checked=true] .k { left: 30px; right: 4px; }
.sq[aria-checked=true]:active .k { left: 20px; }

Segmented pill from :has(), no JS for position:

.seg:has(#hm-1:checked) .pill { transform: translateX(64px); }
.seg:has(#hm-2:checked) .pill { transform: translateX(128px); background: var(--accent); }

One toggle handler for all switches:

document.querySelectorAll('[role=switch]').forEach((b) => b.addEventListener('click', () => {
  const on = b.getAttribute('aria-checked') !== 'true';
  b.setAttribute('aria-checked', String(on));
  report(rowIndex(b), on);
}));

Common mistakes: scoping .lb span { transform: translateY(-50%) } so broadly that it catches the thumb span too; hiding the day/night sky with display (kills the cross-fade); forgetting overflow: hidden on the day/night track so the halo rings bleed.

Details

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

Studied

UseArticle

Let the visitor press the controls and watch the card change. Thick black edges and hard shadows keep it friendly.

All sources