Design Lounge
Install

Nº 402 of 520

Sheen pill buttons

Champagne pill buttons on green-black: a light sheen sweeps across on hover, a conic gradient border turns, and async pills go loading then success.

Preview

Open full size
1280 × 800 · 12.5 KB · 204 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 "Sheen pill buttons" (piece button-sheen-pill) 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

194 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 booking buttons for “Orlé”, a fictional chef’s-counter restaurant. Full pill buttons on a green-black card: a solid champagne pill with a soft gold glow, a dark pill with a 1.5px gold conic gradient border, a quiet text pill, and a round icon pill. On hover a narrow diagonal band of light sweeps once across the pill in 1s, and the border gradient rotates 180°, so the metal looks like it caught the light. The two booking pills are asynchronous: click and the label cross-fades to a spinner with “Holding your table”, the pill width animates to fit, then it turns sage with a drawn check and “Table for 2 · 20:30”. The detail worth copying is that the sweep is a one-shot keyframe on hover, not a transition, so it never runs backwards when the pointer leaves.

Reference behaviour

  1. First frame: centred card. “Orlé” in Italiana 52px champagne, meta line “Chef’s counter · Saturday 14 November · 2 guests”, and on the right a tracked caption “Pill buttons · sheen on hover”.
  2. Live row: “Reserve the table” (solid, large), “Add wine pairing” (gradient edge, medium), “Tasting menu →” (gradient edge, small), “Gift an evening” (quiet, small), heart icon pill (edge, 52px circle, toggle).
  3. Hover or keyboard focus: the sheen band travels from −130% to 130% of the pill’s width over 1000ms, expo-out. The edge gradient’s start angle animates 120° → 300° over 900ms. Solid pills grow their glow.
  4. Press: scale 0.97 in 90ms.
  5. Click “Reserve the table”: aria-busy="true", label swaps to a spinning 18px arc + “Holding your table”; the label fades up 6px in 260ms; the pill width animates from old to new in 420ms.
  6. After 1500ms: aria-busy is removed, the pill gains .ok, turns sage (#c9efd6 → #8fd3a8) with dark green text, a check draws in 420ms, label “Table for 2 · 20:30”. A polite live region says “Table reserved for 2 at 20:30”.
  7. After 3200ms more it morphs back to the idle label. Clicks while busy or in success are ignored.
  8. “Add wine pairing” runs the same cycle: “Adding pairing” → “Pairing added · £68”; its success state is a sage 1.5px border with sage text.
  9. Heart pill toggles aria-pressed; on, it turns rose #e79a8a and fills. The live region says “Saved to favourites” / “Removed from favourites”.
  10. A States sheet shows Solid, Gradient edge and Quiet at Default, Hover (sheen frozen mid-sweep), Pressed, Disabled, Focus; below it a Lifecycle strip: idle → loading → success.
  11. Reduced motion: no sheen, no width tween, no fade, no check draw; the spinner slows to 2400ms per turn.

Structure

1280 × 800, card width min(1060px, 100%), padding 36/44/32, radius 28, 1px line
┌──────────────────────────────────────────────────────────────────────────┐
│ Orlé (Italiana 52)                          PILL BUTTONS · SHEEN ON HOVER │
│ Chef's counter · Saturday 14 November · 2 guests                          │
│─────────────────────────────── 1px line ─────────────────────────────────│
│ (Reserve the table) (Add wine pairing) (Tasting menu →) Gift an evening (♡)│
│   58px solid         52px edge          42px edge        42px quiet  52px │
│─────────────────────────────── 1px line ─────────────────────────────────│
│ STATES        DEFAULT   HOVER   PRESSED   DISABLED   FOCUS                │
│ Solid         (Reserve) …                                     42px pills  │
│ Gradient edge (Pairing) …                                                 │
│ Quiet          Gift …                                                     │
│ LIFECYCLE  (Reserve the table) → (◌ Holding your table) → (✓ Table for 2) │
└──────────────────────────────────────────────────────────────────────────┘
  • Card is main labelled by the h1.
  • Live row: section aria-label="Try the buttons" of button type="button"; each label sits in a span.lbl so it can be swapped.
  • A visually hidden p aria-live="polite" sits after the row.
  • The sheet and lifecycle strip are wrapped in one div inert aria-hidden="true", after a visually hidden description.

Tokens

@property --ang { syntax: "<angle>"; inherits: false; initial-value: 120deg; }
:root {
  --bg: #0f1412;        /* page, green-black */
  --surface: #161c19;   /* card */
  --surface-2: #1d2420; /* edge pill fill */
  --ivory: #f2ebdd;     /* text */
  --ink-2: #a8a293;     /* captions ~7:1 on card */
  --line: rgba(242,235,221,.1);
  --gold-1: #f4e2b8;  --gold-2: #d4b073;  --gold-3: #8f6c3a;   /* champagne ramp */
  --on-gold: #1d160a;   /* text on solid pill ~10:1 */
  --sage: #8fd3a8;      /* success */
  --rose: #e79a8a;      /* favourite on */
  --serif: "Italiana", Georgia, serif;
  --sans: "Urbanist", system-ui, sans-serif;
  --ease: cubic-bezier(.2,.7,.2,1);
  --out: cubic-bezier(.16,1,.3,1);
}

Sizes: L 58px / 34px padding / 16px; M 52 / 28 / 15; S 42 / 20 / 14; icon 52×52; sheet 42 / 20 / 14. Radius always 999px. Gap 10px between icon and label; 18px between pills.

Solid fill: linear-gradient(180deg, #f4e2b8, #d4b073 60%, #c49c5c) + inset 0 1px 0 rgba(255,255,255,.6), inset 0 -1px 0 rgba(90,60,20,.4), 0 10px 26px -12px rgba(212,176,115,.55).

Typography

RoleFamilySizeWeightTrackingCase
WordmarkItaliana52px / 0.94000.01emTitle, --gold-1
MetaUrbanist14px4000Sentence
Pill labelUrbanist14 / 15 / 16px6000.01emSentence
Captions, column headsUrbanist11px / 10px6000.16emUpper
Row labelsUrbanist13px4000Sentence

Italiana is display-only. Labels stay Urbanist 600 so they read at 14px on gold.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
Sheen:hover, :focus-visible::after translateX−130% → 130%1000ms, once—outnone
Border turn:hover—ang120° → 300°900ms—outinstant
Glow:hover (solid)box-shadow y/blur10/26 → 14/32240ms—easeinstant
Press:activescale1 → .9790ms—easenone
Label swapstate changeopacity, translateY0, 6px → 1, 0260ms—outnone
Width fitstate changewidthold px → new px420ms—outinstant
Spinnerbusyrotate0 → 360°800ms linear, looplinear2400ms
Checksuccessstroke-dashoffset24 → 0420ms, 80ms delay—outdrawn

Linear is right for the spinner only.

States

  • Default: as tokens.
  • Hover: sheen sweep, border turn, solid glow grows, quiet gets an 8% champagne wash.
  • Pressed: scale .97.
  • Focus-visible: 2px --gold-1 outline, offset 4px; also plays the sheen once.
  • Disabled: fill #1c211e, text #6f6b61, edge border flat #3a3f3a, no sheen, no glow, cursor: not-allowed.
  • Loading: aria-busy="true", spinner + present-tense label, cursor: progress, clicks ignored.
  • Success: .ok; solid becomes sage gradient with #0d2617 text; edge becomes sage border and sage text; check icon draws.
  • Favourite on: aria-pressed="true", rose, filled heart.

Accessibility

  • Pills are real buttons. Visible label is the name; the heart uses aria-label="Save to favourites" + aria-pressed.
  • Loading sets aria-busy and the live region announces the loading text, then a full sentence on success (“Table reserved for 2 at 20:30”), not the abbreviated label.
  • Do not set disabled while loading; that drops focus. Ignore clicks instead.
  • Contrast: --on-gold on gold ~10:1; ivory on --surface-2 ~15:1; --gold-1 quiet text on card ~13:1; dark green on sage ~10:1.
  • Keyboard: Tab order follows the row; Enter/Space activate; focus plays the sheen so keyboard users get the same moment.
  • Hit targets ≥ 42px.

Responsive rules

  • ≥ 1024: single live row as drawn.
  • 640–1024: the live row wraps; pills keep their sizes.
  • < 860: card padding 26/20, wordmark 42px; the sheet drops the row-label column, labels sit above each row, pills shrink to 36px with 7px padding and 11.5px text; lifecycle arrows hide and the strip wraps.
  • At 375 nothing overflows. Never shrink a live pill below 42px tall.

Acceptance checklist

Always

  • Every button is a full pill (radius 999px).
  • The sheen is an ::after band swept by a one-shot keyframe on hover/focus, clipped by overflow: hidden.
  • The gradient border is a two-layer background (padding-box fill, border-box gradient) on a transparent 1.5px border.
  • The border gradient angle animates via a registered @property.
  • Async buttons go idle → loading (aria-busy) → success → idle, ignoring clicks while busy.
  • Width animates between label lengths instead of jumping.
  • Success is announced in a polite live region with a full sentence.
  • Focus ring 2px, offset 4px; disabled is flat and sheen-free.
  • A states sheet shows default, hover, pressed, disabled, focus, plus the lifecycle, and is inert.
  • Reduced motion removes sweep, fade, width tween and check draw.

This demo

  • Wordmark “Orlé”; live pills Reserve the table, Add wine pairing, Tasting menu, Gift an evening, heart.
  • Loading 1500ms, success held 3200ms.
  • Success labels “Table for 2 · 20:30” and “Pairing added · £68”.
  • Card #161c19 on #0f1412; solid gold #f4e2b8 → #d4b073; success #8fd3a8.

Implementation notes

Sheen and border. Keyframe, not transition, so leaving does not play it in reverse:

.pill { position: relative; isolation: isolate; overflow: hidden; border-radius: 999px;
  transition: --ang 900ms var(--out), width 420ms var(--out); }
.pill::after { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: linear-gradient(105deg, transparent 30%, rgba(255,255,255,.5) 48%,
    rgba(255,255,255,.12) 56%, transparent 70%);
  transform: translateX(-130%); }
.pill:hover::after, .pill:focus-visible::after { animation: sheen 1000ms var(--out); }
@keyframes sheen { to { transform: translateX(130%); } }
.edge { border: 1.5px solid transparent;
  background: linear-gradient(var(--surface-2), var(--surface-2)) padding-box,
    conic-gradient(from var(--ang), #f4e2b8, #8f6c3a 25%, #d4b073 50%, #5a4526 75%, #f4e2b8) border-box; }
.pill:hover { --ang: 300deg; }

On the solid pill the band must sit above the gradient fill but below the label: give the label position: relative; z-index: 1.

Width morph. width: auto can’t transition, so measure:

function morph(btn, html) {
  const lbl = btn.querySelector('.lbl'), w0 = btn.offsetWidth;
  btn.style.width = 'auto'; lbl.innerHTML = html;
  const w1 = btn.offsetWidth;
  btn.style.width = w0 + 'px'; void btn.offsetWidth; btn.style.width = w1 + 'px';
}

Common mistakes:

  • A full-width shimmer looping forever; the sweep plays once per hover.
  • A purple-blue gradient border. The ramp is champagne to bronze only.
  • Swapping to disabled during loading, which kills focus and screen-reader context.
  • Announcing “Table for 2 · 20:30” literally; announce a sentence.
  • Letting the label wrap during the width tween; set white-space: nowrap.

Details

Palette
Type
Italiana · Urbanist
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Wickret

Let the big words move. Letters can break apart and come back as new words.

All sources