Design Lounge
Install

Nº 589 of 605

Link-in-bio bento

A creator's link-in-bio page as a bento of tiles: profile, socials, a drawn latest-work tile, a playable podcast card, a shop drop and a workshop. Tiles press in on tap.

Preview

Open full size
390 × 844 · 11.7 KB · 168 lines

Use it

Free to use · credit appreciated

1 Once, in your project · Node 22.20+

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Link-in-bio bento" (piece card-link-in-bio-bento) 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

208 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 one page a creator links from every profile, built as a bento grid instead of a stack of identical pill buttons. The invented creator is Wren Achterberg, a ceramicist with a podcast and a Sunday-only shop. Each link gets a tile shaped by what it is: a dark profile tile, four square social tiles, a tall “latest work” tile with a drawn celadon vase, an iron-red podcast tile with a waveform that moves when you press play, a sage shop tile with the next drop time, and a workshop tile with a date block. Every tile scales to 0.96 with an inset shadow when pressed and springs back on release. The detail worth copying is that tile size encodes priority: the vase and the podcast get two and three rows, the socials get one small square each.

Reference behaviour

  1. Initial state at 390 × 844: profile tile, the social row, then the work, podcast and shop tiles fill the first screen; the workshop tile starts just above the fold.
  2. Press any tile (pointer-down): it scales to 0.96 over 140ms and its outer shadow becomes a 2px inset shadow. Release: it springs back over 320ms with a slight overshoot (cubic-bezier(.34,1.56,.64,1)).
  3. The profile tile does not press; it is not a link.
  4. Tap the podcast tile: the play glyph becomes a pause glyph, aria-pressed turns true, and the 28 waveform bars start bobbing (scaleY 1 → 0.45, 900ms alternate, each bar offset by −70ms). Tap again to stop.
  5. The first 8 waveform bars are full cream (played), the rest at 55% opacity, matching “12:04 / 41:30”.
  6. Links are real <a> elements; in the demo their navigation is prevented.
  7. At ≥ 700px wide the grid becomes four columns with named areas (see Responsive rules).

Structure

390 × 844, 16px side padding, 62px top, tiles gap 10, radius 22
┌──────────────────────────────────────┐
│ ┌──────────────────────────────────┐ │
│ │ (WA)  Wren Achterberg  (Limelight 31)│ profile, span 2
│ │       @wren.makes · Leith studio  │ │ dark #1F2B27
│ │ Stoneware, slow radio, and a…     │ │
│ │ (● Taking commissions)(21.4k…)    │ │
│ └──────────────────────────────────┘ │
│ [Diary] [Films] [Letters] [Email]    │ 4 squares, span 2
│ ┌───────────────┐ ┌───────────────┐  │
│ │  drawn vase   │ │ KILN HOURS    │  │ work: span 3 rows
│ │               │ │ Glaze is a…   │  │ podcast: span 2 rows
│ │               │ │ ▌▌▐▌▐▐▌ wave  │  │
│ │───────────────│ │ (▶) 12:04/41:30│ │
│ │ LATEST WORK   │ ├───────────────┤  │
│ │ Tidal series  │ │ SHOP Sun 10am │  │ shop: 1 row
│ └───────────────┘ └───────────────┘  │
│ ┌──────────────────────────────────┐ │
│ │ [09 NOV] Wheel class for two [2 spots]│ workshop, span 2
│ └──────────────────────────────────┘ │
│        Achterberg Studio · address    │ footer
└──────────────────────────────────────┘
  • <main> max-width 720px, centred. .grid is CSS grid, repeat(2, minmax(0,1fr)), grid-auto-rows: minmax(84px, auto), gap 10px.
  • <section class="t pro" aria-label="Profile">: avatar disc (initials in Limelight on an iron-red radial gradient, 3px ground ring + 1px outer line), <h1> name, handle line, bio <p> with one italic word, two status pills.
  • <nav class="soc" aria-label="Elsewhere">: four <a class="t"> squares (aspect-ratio:1), each a 40px icon well plus a label.
  • <a class="t work">: drawn image area (flex:1, min 200px) with an inline SVG vase, then a cream caption block.
  • <button class="t pod" aria-pressed>: kicker, title, episode line, .wave (28 bars), controls row with a 40px play disc and time.
  • <a class="t shop"> and <a class="t ws"> (workshop with a 58 × 62 date block and a “2 spots” pill).
  • Every tile that is a link shows a 32px round arrow chip in its top-right corner.

Tokens

:root {
  /* colour */
  --ground: #e9ece1;   /* page, icon wells */
  --ink: #1f2b27;      /* text, profile tile, date block */
  --ink-2: #5a6660;    /* secondary text on light tiles */
  --cream: #f5f1e6;    /* text on dark, work caption, workshop tile */
  --paper: #f9f7f1;    /* social tiles */
  --sage: #c9d8c5;     /* shop tile */
  --iron: #a63d2a;     /* podcast tile, focus ring */
  --iron-2: #8e3121;   /* "2 spots" text */
  --moss-ink: #a9b8b0; /* handle on the profile tile */
  --blush: #f2d3c6;    /* italic word in the bio */
  --spots-bg: #f3dcd3;

  /* type */
  --display: "Limelight", Georgia, serif;
  --serif: "Cormorant Garamond", Georgia, serif;

  /* layout */
  --pad: 16px;
  --gap: 10px;
  --r: 22px;      /* tiles */
  --r-s: 14px;    /* date block */
  --r-icon: 12px; /* icon wells */
  --tap: 40px;    /* play disc, icon wells */

  /* depth */
  --sh: 0 1px 0 rgba(31,43,39,.06), 0 10px 24px -16px rgba(31,43,39,.35);
  --sh-press: inset 0 2px 8px rgba(31,43,39,.18);

  /* motion */
  --t-press: 140ms;
  --t-release: 320ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --spring: cubic-bezier(.34, 1.56, .64, 1);
  --t-wave: 900ms;
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
NameLimelight31px4001+0.01emTitle
Avatar initialsLimelight28px40010caps
Tile headline (work)Limelight22px4001.050sentence
Shop timeLimelight30px40010sentence
Date numeralLimelight26px40010—
BioCormorant Garamond21px5001.250sentence, max 30ch
Podcast titleCormorant Garamond21px7001.080sentence
Workshop titleCormorant Garamond20px7001.10sentence
Body / metaCormorant Garamond15–17px500–6001.350sentence
KickerCormorant Garamond12px6001+0.16emUPPERCASE
Social labelCormorant Garamond14px60010Title

Set font-variant-numeric: lining-nums on the body; Cormorant’s default old-style 1 reads as a capital I in “10am”.

Motion

ElementTriggerPropertyFrom → ToDurationEasingReduced motion
.tpointer-down / :activetransform, box-shadow1, --sh → .96, --sh-press140ms--easeinstant, no scale
.treleasetransform, box-shadow.96 → 1320ms--springinstant
.wave ipodcast playingscaleY1 → .45, alternate, infinite900ms--easestatic bars
play glyphpodcast toggleswaptriangle ↔ two bars0—same

The waveform is the only loop and only runs after the visitor presses play.

States

  • Rest: tile shadow --sh.
  • Pressed: transform: scale(.96), inset shadow; mirrored by a .press class on pointerdown because iOS Safari applies :active unreliably.
  • Focus-visible: 3px --iron outline, 3px offset, on every tile.
  • Podcast playing: .on class, aria-pressed="true", label “Pause Kiln Hours episode 38”, bars bobbing.
  • Commission status: the 8px dot in the first pill is #7fd19b when open; swap for --ink-2 and the text “Books closed” when not.
  • Low stock: the workshop “2 spots” pill uses --iron-2 on --spots-bg.

Accessibility

  • One <h1> (the creator’s name). Tile headlines are <h2> only inside links; the podcast tile is a <button>, so its title is a <span> (buttons may only hold phrasing content).
  • Every link has an aria-label that reads as a sentence (“Wheel class for two, Saturday 9 November, 2 spots left”); arrow chips and drawings are aria-hidden.
  • The podcast button is a toggle with aria-pressed and a label that flips between Play and Pause.
  • Socials sit in <nav aria-label="Elsewhere">.
  • Contrast: cream on iron 5.6:1, cream on ink 13.0:1, --ink-2 on cream 5.3:1, --moss-ink on ink 7.1:1, #33433d on sage 7.0:1.
  • Hit targets: every tile is at least 82px square at 390 (74px at 360); the play disc is 40px but the whole tile is the button.

Responsive rules

  • 390: two columns as drawn.
  • 360: same; tiles are 159px wide and social squares 74px; the bio wraps to three lines; nothing changes size.
  • ≥ 700px: four columns and named areas, max-width 720px:
.grid { grid-template-columns: repeat(4, minmax(0,1fr));
  grid-template-areas: "pro pro work pod" "pro pro work pod" "ws ws work shop" "soc soc soc soc"; }

Social tiles drop aspect-ratio and become 104px tall; profile pills sit at the tile’s bottom (margin-top:auto).

  • ≥ 1280: same as 700, centred on the ground.

Acceptance checklist

Always

  • Tiles use a 2-column grid on phones with a 10px gap and 22px radius.
  • Tile spans encode priority: the hero link spans 3 rows, the media tile 2, small links 1.
  • Every pressable tile scales to 0.96 on press (140ms) and springs back (320ms, overshoot curve).
  • The profile tile is not a link and does not press.
  • The media tile is a toggle button with aria-pressed, a glyph swap and an animated waveform only while playing.
  • Links are real anchors with sentence-style labels; decorative icons and drawings are hidden from assistive tech.
  • Focus rings are visible on every tile.
  • Nothing scrolls sideways at 360 or 390.
  • Reduced motion removes the press scale and the waveform loop.

This demo

  • Name “Wren Achterberg”, handle “@wren.makes · Leith studio”, pills “Taking commissions” and “21.4k followers”.
  • Socials: Diary, Films, Letters, Email.
  • Work tile: “Tidal series · 14 pieces · celadon over iron clay” with a drawn vase.
  • Podcast: “Kiln Hours · Glaze is a weather report · Ep 38 · with Sol Okonjo”, 12:04 / 41:30.
  • Shop “Sun 10am · 36 pieces drop · mugs from £38”; workshop “09 Nov · Wheel class for two · £140 per pair · 2 spots”.

Implementation notes

Press feedback that works on iOS. Use both :active and a pointer class, and give press and release different clocks:

.t { transition: transform 320ms cubic-bezier(.34,1.56,.64,1), box-shadow 320ms cubic-bezier(.2,.7,.2,1); }
.t:active, .t.press { transform: scale(.96); box-shadow: inset 0 2px 8px rgba(31,43,39,.18);
  transition-duration: 140ms; transition-timing-function: cubic-bezier(.2,.7,.2,1); }
tile.addEventListener('pointerdown', () => tile.classList.add('press'));
['pointerup', 'pointerleave', 'pointercancel'].forEach((ev) => tile.addEventListener(ev, () => tile.classList.remove('press')));

A vase without an image. Two stacked SVG paths share one silhouette: the clay body with a horizontal gradient, and the glaze clipped by a wavy lower edge that reads as drips:

<path d="M44 6h32v8c0 6 26 20 30 52 4 34-14 64-46 78C28 130 10 100 14 66 18 34 44 20 44 14z" fill="url(#clay)"/>
<path d="M44 6h32v8c0 6 26 20 30 52 1 8 0 15-2 22-6-4-9 6-15 2-5-3-8 8-14 4-6-4-10 7-16 3-6-4-9 5-15 1-5-4-8 4-12 0C11 80 12 72 14 66 18 34 44 20 44 14z" fill="url(#glaze)"/>

Offset loop phases with negative delays so the waveform starts mid-motion instead of every bar moving in lockstep: animation-delay: calc(var(--i) * -70ms).

Common mistakes: putting headings or <div>s inside the <button> tile; making every tile the same size (the page turns back into a list of pills); leaving Cormorant on old-style numerals for times and prices.

Details

Palette
Type
Limelight · Cormorant Garamond
Motion
Subtle motion
Build
Quick build
Designed bySusan AcharyaKathmandu

Studied

Mendo

A control. It studies this site for quiet, not for a show.

All sources