Design Lounge
Install

Nº 535 of 543

Card grid with sticker filters

Six bordered work cards in a 3×2 grid, each with a flat colour band and a drawn mark; hover pushes a card into its shadow, and pressed filter tags hide the rest with a 160ms fade.

Preview

Open full size
1280 × 800 · 8.6 KB · 111 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 "Card grid with sticker filters" (piece card-grid-brutal) 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

155 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. When a kit is locked, map the colours onto the kit tokens and keep the 2px ink borders and the hard 0-blur shadows. The drawn marks are placeholders for this product’s own work.

What it is

The work grid of Halden Studio, a fictional design studio, in a neo-brutalist system. A 56px uppercase title with a kicker sits left; a live count and four filter tags sit right. Six cards fill a three-column grid: each is a bordered box with a 4px hard shadow, a 148px flat colour band (yellow, blue, pink or cream) holding one drawn mark in ink, then a title, one sentence, and a meta row with the category and a bordered year tag. Hovering a card pushes it into its shadow, the same rule as every button in the family, so the whole page has one idea of “pressable”. Pressing a filter tag turns it ink-on-cream, fades the other cards out over 160ms, removes them from the grid so the rest close up, and updates the count. The detail worth copying is that the cards’ colour comes only from their bands: the body is always cream, so six loud cards still read as one set.

Reference behaviour

  1. First frame: “HALDEN STUDIO · 2024-2026 / WORK”, count “6 projects”, tags All (pressed), Identity, Web, Print. Six cards: Nordpost parcel lockers (yellow, identity, 2026), Tally payroll site (blue, web, 2026), Stapler issue 12 (pink, print, 2025), Lume theatre (cream, identity, 2025), Asar money app (yellow, web, 2025), Market zine issue 3 (blue, print, 2024).
  2. Hover a card: it translates (4px, 4px) and its shadow collapses to 0 in 120ms. Leave: back.
  3. Hover a tag: it moves (3px, 3px) into its 3px shadow. Press a tag: aria-pressed="true" on it and false on the others; it fills ink with cream text.
  4. Press “Identity”: the four non-identity cards fade to 0 over 160ms, then are set hidden, so the two identity cards sit side by side in the first row. The count reads “2 projects”.
  5. Press “All”: every card returns (hidden removed, opacity back to 1 over 160ms).
  6. If a filter matches nothing, a dashed bordered line “Nothing in that category yet.” shows under the grid.
  7. Nothing animates on load.

Structure

1280 × 800, bg #F6F1E4, wrap max 1200, padding 32/40
┌ head ───────────────────────────────────────────────────────────────────────────┐
│ HALDEN STUDIO · 2024-2026                 6 PROJECTS [All][Identity][Web][Print]│
│ WORK 56/.95                                                                     │
├ grid 3 × 1fr, gap 24 ───────────────────────────────────────────────────────────┤
│ ┌ band yellow 148 ─┐  ┌ band blue ───────┐  ┌ band pink ───────┐                │
│ │   (drawn mark)   │  │   (drawn mark)   │  │   (drawn mark)   │                │
│ ├──────────────────┤  ├──────────────────┤  ├──────────────────┤                │
│ │ NORDPOST PARCEL… │  │ TALLY PAYROLL…   │  │ STAPLER ISSUE 12 │  4px shadows   │
│ │ one sentence     │  │ one sentence     │  │ one sentence     │                │
│ │ IDENTITY   [2026]│  │ WEB        [2026]│  │ PRINT      [2025]│                │
│ └──────────────────┘  └──────────────────┘  └──────────────────┘                │
│ (second row: cream, yellow, blue)                                               │
└─────────────────────────────────────────────────────────────────────────────────┘
  • main.wrap → div.head (h1 with small, div.filters[role=group] with span.count[aria-live] and four button.tag[aria-pressed][data-f]), div.grid#grid, p.empty.
  • Each a.card[data-k] (colour class y, b, p or c) → div.art[aria-hidden] with an inline SVG, div.body (h2, p, div.meta with two spans).

Tokens

:root {
  --bg: #f6f1e4;  --surface: #fffaf0;  --surface-2: #ece4d0;
  --ink: #17151a;  --ink-2: #4a4650;  --ink-3: #76717d;
  --yellow: #ffd23f;  --blue: #2f5bff;  --blue-ink: #fffdf8;  --pink: #ff5c9a;

  --display: "Archivo Black", Impact, sans-serif;
  --sans: "Archivo", system-ui, sans-serif;

  --bw: 2px;  --off: 4px;  --off-tag: 3px;
  --r: 4px;  --r-card: 6px;
  --band-h: 148px;  --gap: 24px;  --tag-h: 40px;

  --t-micro: 120ms;  --t-hide: 160ms;
  --ease: cubic-bezier(.2, .7, .2, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
TitleArchivo Black56px4000.95−0.02emUPPERCASE
Card titleArchivo Black20px4001.05−0.01emUPPERCASE
KickerArchivo13px7001+0.12emUPPERCASE, --ink-3
Card sentenceArchivo14px5001.50sentence, --ink-2
Filter tagArchivo13px7001+0.06emUPPERCASE
CountArchivo12px7001+0.1emUPPERCASE, --ink-2
Meta, year tagArchivo11px7001+0.1emUPPERCASE
Empty lineArchivo16px7001.50sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasing
.cardhovertransform, box-shadownone, 4px 4px 0 → translate(4px,4px), 0 0 0120ms--ease
.taghovertransform, box-shadownone, 3px 3px 0 → translate(3px,3px), 0 0 0120ms--ease
.card.outfilteropacity1 → 0, then hidden160ms--ease
.card returningfilteropacity0 → 1 after hidden is removed160ms--ease

Reduced motion: transitions 1ms, and filtered cards are hidden at once instead of after the fade.

States

  • Card default: cream body, coloured band, 4px shadow. Hover: pushed into the shadow. Focus-visible: 2px blue outline at 3px offset.
  • Tag default: cream, 3px shadow. Pressed: --ink fill, --bg text, shadow unchanged. Hover: pushed.
  • Count: “N projects”, or “1 project”; live region.
  • Empty: a dashed 2px ink border box with one sentence, only when no card matches.

Accessibility

  • Filters are buttons with aria-pressed inside a role="group" labelled “Filter the work”; the count is aria-live="polite" so the result of a press is announced.
  • Cards are links; the title is real text in an h2; the drawn mark is aria-hidden and the band colour carries no meaning on its own (the category is written in the meta row).
  • Hidden cards use the hidden attribute, so they leave the tab order and the accessibility tree.
  • Contrast: ink on cream 15.2:1; --bg on --ink 15.2:1 (pressed tag); --ink-2 on --surface 9.1:1; --ink-3 kicker on cream 4.9:1.
  • Hit targets: tags 40px tall; cards are whole-link targets; the push is 4px, within vestibular guidance.

Responsive rules

  • ≥ 1280: three columns, gap 24, band 148px; two rows fit in 800px.
  • 1024–1279: three columns, gap 20, band 132px, title 48px.
  • 768–1023: two columns; the head stacks (title, then filters).
  • < 640: one column, padding 20px, title 40px, filters scroll horizontally in one row (overflow-x: auto, no wrap), band 160px. Shadow offsets stay 4px / 3px.

Acceptance checklist

Always

  • Every card and tag has a 2px --ink border; shadows are hard with no blur: 4px on cards, 3px on tags; cards use the family card radius, tags the family radius.
  • Card bodies are always --surface; colour appears only in the band, from a set of at most three accent fills plus cream.
  • Hovering a card translates it exactly (4px, 4px) and collapses its shadow on the same 120ms clock.
  • Filter tags use aria-pressed; exactly one is pressed; the pressed tag is ink-on-cream.
  • Filtering fades non-matching cards out over 160ms, then hides them so the grid closes up; the live count updates; an empty result shows the dashed line.
  • The category is written in each card’s meta row, not carried by colour alone.
  • Focus rings are visible on tags and cards, outside the shadows.
  • Reduced motion: 1ms transitions, cards hide at once.

This demo

  • Six cards for Halden Studio with the titles, categories and years above; the count starts at “6 projects”.
  • Tags All, Identity, Web, Print; Identity leaves Nordpost and Lume; Print leaves Stapler issue 12 and Market zine.

Implementation notes

One push for cards and buttons alike:

.card { border: var(--bw) solid var(--ink); box-shadow: var(--off) var(--off) 0 var(--ink);
  transition: transform var(--t-micro) var(--ease), box-shadow var(--t-micro) var(--ease), opacity var(--t-hide) var(--ease); }
.card:hover { transform: translate(var(--off), var(--off)); box-shadow: 0 0 0 var(--ink); }
.card.out { opacity: 0; pointer-events: none; }
.card[hidden] { display: none; }

Fade, then hide, so the grid closes up after the fade and not during it:

cards.forEach((c) => {
  const show = f === 'all' || c.dataset.k === f;
  if (show) { c.hidden = false; requestAnimationFrame(() => c.classList.remove('out')); }
  else { c.classList.add('out'); reduce ? (c.hidden = true) : setTimeout(() => { if (c.classList.contains('out')) c.hidden = true; }, 160); }
});
count.textContent = n + (n === 1 ? ' project' : ' projects');

The drawn marks are three or four shapes in one inline SVG per card, ink strokes at 3px on the band colour. Replace them with the product’s real images; keep the band as a flat fill behind a cut-out, not a photo with a tint.

Common mistakes: colouring the card body as well as the band (then six cards shout at each other); a blurred hover shadow; display: none during the fade (the fade never shows); a grid that leaves a gap where a hidden card was (use hidden, not visibility); a tag row that wraps to three lines on a phone instead of scrolling.

Details

Palette
Type
Archivo Black · Archivo
Motion
Subtle motion
Build
Quick build
Designed bySusan AcharyaKathmandu

Studied

60fps

Show one small moment at a time, and let the tags do the sorting.

All sources