Design Lounge
Install

Nº 251 of 520

Follow, unfollow and save buttons

A set of social buttons: Follow morphs to Following and shows Unfollow only on a later hover, a bell appears after follow, and Save fills its ribbon and shows an undo toast.

Preview

Open full size
1280 × 800 · 16.6 KB · 267 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 "Follow, unfollow and save buttons" (piece button-follow-bookmark) 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

231 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 relationship buttons of “Driftline”, a community for film photographers, shown on a cool grey page. On the left is a creator card for Wren Achebe with a primary Follow button and an “Also on Driftline” list with three compact follow pills. On the right is a story card with a Save (bookmark) button. Follow turns into Following with a drawn check. The button keeps its width because all three labels share one grid cell. Unfollow is offered only when the pointer comes back after leaving, so the click that followed can never accidentally unfollow. Following also reveals a 44px bell for post notifications. Save fills its ribbon top to bottom in cobalt, nudges it down 3px, ticks the count, and raises a dark toast with Undo. These are the four social verbs, follow, unfollow, notify and save, done crisply in one family.

The heart and like burst are optimistic-like-button. Do not add a like here.

Reference behaviour

  1. First frame: the creator card (360px) and the story card (500px) side by side, centred, gap 24px.
  2. Creator card: a 64px avatar “WA” on #c9d3ff, “Wren Achebe” (23px), “@wren.achebe · Lisbon”, a three-line bio, then “12,408 followers 311 following”. Then a black pill ”+ Follow”, 44px tall, min-width 128px. The bell is collapsed to width 0 and is inert.
  3. “Also on Driftline” lists Ilse Varga (already Following), Noor Castell (Follow) and Mateo Brandt (Follow). These are 36px compact pills.
  4. Story card: kicker “Story · 8 min read · 2 Oct” with “Story” in cobalt, title “Forty mornings on the river, one roll each” (30px), a two-line excerpt, a four-frame contact strip, then a footer with “Save 214” on the left and “38 replies · 1.9k reads” on the right.
  5. Click Follow: the label ”+ Follow” slides up 6px and fades. “Following” with a check icon slides in from 6px below, and the check stroke draws over 320ms after an 80ms delay. The fill goes from ink to white with a 1px --line inset ring. The followers count becomes 12,409 and drops in from 8px above. The bell expands from width 0 to 44px with a fade. aria-pressed="true".
  6. While the pointer stays on the button after that click, it keeps reading Following. When the pointer leaves (or focus blurs), the button is armed.
  7. Hover an armed Following button: the label crossfades to “Unfollow”, the background goes to --danger-soft, the text goes to --danger, and the ring becomes rgba(207,42,31,.35). Mouse out returns it to Following.
  8. Click while following: the button returns to Follow immediately, the count decrements, the bell collapses and resets to off, and a toast appears: “Unfollowed Wren” with Undo. Undo restores Following (already armed) and the count.
  9. Click the bell: it fills cobalt with a white filled glyph and rings (rotate 16° → −12° → 7° → −3° → 0 over 600ms). aria-pressed="true". Toast: “You’ll hear when Wren posts” (no Undo). Clicking again turns it off with the toast “Notifications off for Wren”.
  10. Compact pills in the list follow the same rules (Follow → Following → armed → Unfollow + toast with Undo). They don’t affect the follower count or the bell.
  11. Click Save: the ribbon’s filled copy is revealed top to bottom with clip-path over 380ms. The ribbon drops 3px and stretches to 1.06 in Y, then settles (420ms after a 200ms delay, overshoot easing). The label becomes “Saved”, the count becomes 215 and rolls, and the pill turns --accent-soft with cobalt text. Toast: “Saved to Reading list” with Undo.
  12. Click Saved: it un-saves (ribbon empties, 214, “Save”). Toast: “Removed from Reading list” with Undo, which re-saves.
  13. The toast slides up 14px and fades in over 280ms at bottom: 28px, centred. It hides after 4000ms. Hovering pauses the timer, and leaving restarts a 2000ms timer. Escape hides it. A new toast replaces the current one.

Structure

1280 × 800, two cards centred as a group (360 + 24 + 500)
┌──────────────────────────────────────────────────────────────────┐
│   ┌ creator 360 ─────────────┐ ┌ story 500 ──────────────────────┐ │
│   │ (WA) Wren Achebe          │ │ Story · 8 min read · 2 Oct     │ │
│   │      @wren.achebe · Lisbon│ │ Forty mornings on the river,   │ │
│   │ bio 14.5px, 3 lines       │ │ one roll each        (30px)    │ │
│   │ 12,408 followers 311 fol… │ │ excerpt 15px                   │ │
│   │ [ + Follow ] (bell 0→44)  │ │ ▮▮▮▮ contact strip, 4 frames   │ │
│   │───────────────────────────│ │────────────────────────────────│ │
│   │ ALSO ON DRIFTLINE         │ │ [ribbon Save 214]  38 replies… │ │
│   │ (IV) Ilse Varga [Following]│ └────────────────────────────────┘ │
│   │ (NC) Noor Castell [Follow]│                                   │
│   │ (MB) Mateo Brandt [Follow]│                                   │
│   └───────────────────────────┘                                   │
│                 ┌ toast ───────────────────────────┐              │
│                 │ ribbon  Saved to Reading list  Undo │ bottom 28px │
└──────────────────────────────────────────────────────────────────┘
  • <main class="stage"> is a grid 360px 500px, gap 24px, align-items: start.
  • The creator card is a <section aria-labelledby="name">: .me (avatar + <h1 id="name"> + handle), .bio, .stats (follower <b id="followers">), .acts (Follow + bell), and .sugg (<h2> + <ul> of rows, each with avatar, .who, and button.follow.sm).
  • The story card is an <article>: .kick, <h2>, <p>, .strip (decorative, four <i> frames drawn with gradients), .foot (button.save + .meta).
  • button.follow > .lab > span.l1 | span.l2 | span.l3, all stacked in one grid cell.
  • button.save > .rib holds two identical ribbon SVGs: an outline, and a filled copy above it that is clipped.
  • .toast[role=status] is fixed at bottom centre: icon, text, button#undo.

Tokens

:root {
  --bg: #ebedf1;            /* cool page */
  --surface: #ffffff;       /* cards, Following fill */
  --surface-2: #f4f5f8;     /* hovers, list dividers */
  --line: #dfe3ea;
  --ink: #0d1015;           /* Follow fill, toast */
  --ink-2: #4b5261;         /* bio, excerpt */
  --ink-3: #687080;         /* handles, meta, counts at rest */
  --accent: #2443f5;        /* saved ribbon, bell on, focus ring, kicker */
  --accent-soft: #e7ebff;   /* saved pill */
  --danger: #cf2a1f;        /* unfollow hover text */
  --danger-soft: #fdeceb;   /* unfollow hover fill */

  --display: "Funnel Display", system-ui, sans-serif;
  --sans: "Funnel Sans", system-ui, sans-serif;

  --r-card: 20px;
  --r-pill: 999px;
  --h-btn: 44px;
  --h-btn-sm: 36px;

  --t-micro: 160ms;
  --t-morph: 240ms;
  --t-fill: 380ms;
  --t-toast: 280ms;
  --hold-toast: 4000ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-pop: cubic-bezier(.34, 1.56, .64, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingNotes
Creator nameFunnel Display23px6001.15−0.015em
Story titleFunnel Display30px6001.12−0.02em25px under 480px
Avatar initialsFunnel Display22px / 13px60010large / list
BioFunnel Sans14.5px4001.550--ink-2
ExcerptFunnel Sans15px4001.60--ink-2
Handle, metaFunnel Sans13–14px4001.50--ink-3
Stats numbersFunnel Sans14px6001.50tabular-nums, --ink
Follow labelFunnel Sans15px / 13.5px60010main / compact
Save labelFunnel Sans14.5px60010count 500 tabular-nums
Section labelFunnel Sans12px60010.08emUPPERCASE --ink-3
ToastFunnel Sans14.5px4001.50Undo 600 #c3ceff

Motion

ElementTriggerPropertyFrom → ToDurationEasingReduced motion
Follow fill/colour/ringtogglebackground, color, box-shadowink → white + ring240ms--ease1ms
.l1 / .l2 / .l3state, armed hoveropacity, translateY0, ±6px ↔ 1, 0160ms / 240ms--ease / --ease-out1ms
check strokefollowstroke-dashoffset20 → 0320ms, delay 80ms--easedrawn
follower count±translateY, opacity−8px, 0 → 0, 1260ms--ease-outnone
bellfollow / unfollowwidth, opacity0, 0 ↔ 44px, 1240ms / 160ms--ease-out1ms
bell glyphnotify onrotate0 → 16° → −12° → 7° → −3° → 0600ms--easenone
ribbon fillsaveclip-pathinset(0 0 100% 0) → inset(0)380ms--ease1ms
ribbonsavetranslateY, scaleY0 → 3px, 1.06 (45 %) → 0420ms, delay 200ms--ease-popnone
save count±translateY, opacity−8px, 0 → 0, 1260ms--ease-outnone
toastshow / hideopacity, translateY0, 14px ↔ 1, 0280ms--ease / --ease-out1ms
any button:activescale1 → .96160ms--easekept

The roll animations restart on every change (remove class, reflow, add). The reduced-motion rule sets every animation and transition to 1ms with no delay, so states still land and nothing travels.

States

  • Follow (rest): ink fill #0d1015, white label, plus icon. Hover #262b35. Active scale .96.
  • Following: white fill, ink label, check icon, 1px inset --line ring, aria-pressed="true".
  • Following, armed, hover: label “Unfollow”, --danger-soft fill, --danger text, 1px ring rgba(207,42,31,.35).
  • Following, not armed: hover shows nothing new. This is the state right after the click.
  • Bell off: 44px circle, 1px --line ring, outline glyph, hover --surface-2. Bell on: --accent fill, white filled glyph.
  • Save (rest): transparent, ink label, outline ribbon, count --ink-3. Hover --surface-2.
  • Saved: --accent-soft fill, cobalt label, count and ribbon (filled).
  • Toast: ink surface, 14px radius, shadow 0 14px 34px rgba(13,16,21,.28). Undo is 36px tall, hover rgba(255,255,255,.1).
  • Focus-visible: 2px --accent outline, offset 3px. Inside the toast it is #c3ceff with offset −2px.

Accessibility

  • Follow buttons are toggle buttons (aria-pressed). Their aria-label is “Follow Wren” or “Following Wren. Press to unfollow”. The visible Unfollow label is a hover hint, so keyboard and screen reader users get the same meaning from the label.
  • Keyboard users arm the button by leaving it: blur arms it, the same as pointerleave. Tab away and back, and the focus ring shows on the Following state. Enter unfollows. Undo is in the toast.
  • The bell is inert while not following, so it is not a tab stop. Its label is “Notify me when Wren posts” or “Notifications on for Wren. Press to turn off”.
  • Save’s aria-label carries state and count: “Save story, 214 saves” or “Saved, 215 saves”.
  • The toast is role="status" (polite). Focus is not moved into it. Undo is reachable by Tab, and Escape closes it.
  • Hover pauses the toast timer so pointer users can reach Undo.
  • Contrast: --ink-3 on white is 4.98:1. Cobalt on --accent-soft is 5.55:1. --danger on --danger-soft is 4.59:1. #c3ceff on the ink toast is 12.3:1.
  • Hit targets: Follow and Save are 44px, compact pills 36px (pointer web; raise to 44px on touch), the bell 44px, Undo 36px.

Responsive rules

  • ≥ 1280: two columns as specified.
  • 1024–1279: unchanged (884px).
  • ≤ 920: one column, max 500px wide. The creator card comes first.
  • < 480: card padding 20px, story title 25px, the reads and replies meta is hidden. The toast stays inside 100vw − 32px.
  • Compact pills keep their labels at every width. Never collapse Follow to an icon.

Acceptance checklist

Always

  • Follow, Following and Unfollow share one width. The labels are stacked in one grid cell, and the button never resizes.
  • Unfollow appears only on hover after the pointer has left once (or after blur). It never appears on the click that followed.
  • Unfollow is instant and offers Undo in a toast. There is no confirm dialog.
  • Notify is only reachable while following, and it resets when you unfollow.
  • Save fills its ribbon top to bottom with a clip, not an opacity fade.
  • Counts change on the same frame as the click.
  • Every toggle uses aria-pressed and a label that states the next action or the state.
  • The toast pauses on hover and closes on Escape.

This demo

  • Creator: Wren Achebe, @wren.achebe · Lisbon, 12,408 followers, 311 following.
  • List: Ilse Varga (Following), Noor Castell, Mateo Brandt.
  • Story: “Forty mornings on the river, one roll each”, Save count 214.
  • The Follow min-width is 128px. The bell grows 0 → 44px.
  • Toast copy: “Saved to Reading list”, “Unfollowed Wren”, “You’ll hear when Wren posts”.
  • The accent is #2443f5. Unfollow uses #cf2a1f on #fdeceb.

Implementation notes

Width-stable labels. Stack every label in the same grid cell. The button sizes to the widest one, so the morph is a crossfade, not a resize.

.follow .lab { display: grid; }
.follow .lab > span { grid-area: 1 / 1; display: flex; align-items: center; justify-content: center; gap: 6px;
  transition: opacity 160ms var(--ease), transform 240ms var(--ease-out); }
.follow .l2, .follow .l3 { opacity: 0; transform: translateY(6px); }
.follow[aria-pressed=true] .l1 { opacity: 0; transform: translateY(-6px); }
.follow[aria-pressed=true] .l2 { opacity: 1; transform: none; }
.follow[aria-pressed=true].armed:hover .l2 { opacity: 0; transform: translateY(-6px); }
.follow[aria-pressed=true].armed:hover .l3 { opacity: 1; transform: none; }

Arming. Clear the armed flag on every toggle, and set it when the pointer leaves or focus blurs. This one class prevents a double-click from following and unfollowing.

btn.addEventListener('pointerleave', () => { if (pressed(btn)) btn.classList.add('armed'); });
btn.addEventListener('blur',         () => { if (pressed(btn)) btn.classList.add('armed'); });
btn.addEventListener('click', () => {
  const on = !pressed(btn);
  btn.setAttribute('aria-pressed', on); btn.classList.remove('armed');
  if (!on) showToast(`Unfollowed ${name}`, () => { btn.setAttribute('aria-pressed', true); btn.classList.add('armed'); });
});

Ribbon fill. Two copies of the same path. The top one is filled and clipped from the bottom. Transition clip-path, then delay the drop until the fill is mostly done.

.rib { position: relative; width: 22px; height: 22px; }
.rib svg { position: absolute; inset: 0; }
.rib .fill { color: var(--accent); clip-path: inset(0 0 100% 0); transition: clip-path 380ms var(--ease); }
.rib .fill path { fill: currentColor; }
.save[aria-pressed=true] .fill { clip-path: inset(0); }
.save[aria-pressed=true] .rib { animation: drop 420ms 200ms var(--ease-pop) both; }
@keyframes drop { 45% { transform: translateY(3px) scaleY(1.06); } }

Common mistakes:

  • Showing Unfollow the instant the button becomes Following. The user’s pointer is still on it.
  • Measuring and animating width between labels. That causes jank and reflows neighbours.
  • A confirm modal for unfollow. Undo is kinder and faster.
  • Leaving the bell focusable while it is collapsed to 0px.
  • Colour-only saved state. The label also changes to Saved.
  • Using the brand accent for Follow. Follow is ink. Cobalt is reserved for saved and notified, the things you own.

Details

Palette
Type
Funnel Display · Funnel Sans
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