Design Lounge
Install

Nº 171 of 520

App icon shelf gallery

A museum-style gallery of 36 SVG app icons on frosted glass shelves, with category, colour and finish filters and a detail sheet with a palette strip.

Preview

Open full size
1280 × 800 · 22.3 KB · 300 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 "App icon shelf gallery" (piece app-icon-shelf-gallery) 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

283 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

Studied from icon.museum: the idea of a curated app-icon collection where every icon sits on a long frosted glass shelf with a faint reflection underneath, and one click opens a quiet label card with the icon large, its details and its colour palette. This rebuild is “Vitrine”, an invented collection of 36 original icons drawn in inline SVG (no copied artwork). The page is warm bone paper with near-black ink and one vermilion accent. The detail worth copying is the shelf: a 15px translucent plank with backdrop blur that sits in front of each icon’s -webkit-box-reflect, so the reflection reads as glass, not as a second icon.

Reference behaviour

  1. First frame at 1280×800: a 64px sticky header, a two-part intro (serif headline left, 15px paragraph right), a filter bar, and three full shelves of eight icons visible.
  2. The header search field shows a rotating hint: “Search 36 icons”, “Search 9 categories”, “Search 9 colours”, “Search 9 studios”. It swaps every 2.6s with a 250ms fade-and-rise. Under reduced motion it stays on “Search 36 icons”.
  3. Pressing / anywhere (outside the dialog) focuses the search field. Typing filters by app name, category or colour family as you type.
  4. The category chips row starts with “All 36” pressed. Each chip shows its count in a lighter weight. Clicking a chip makes it the only pressed chip.
  5. The colour row has nine 24px swatches: red, orange, yellow, green, teal, blue, violet, pink, black and white. Clicking a swatch selects it (2px ink ring at 4px offset). Clicking it again clears it.
  6. The finish segmented control has All, Flat, Glossy, Dark. Exactly one is pressed.
  7. All filters combine with AND. The count at the right of the filter bar reads “36 on display”, “5 on display”, “1 on display”.
  8. When the filters match nothing, the shelves are replaced by an empty state: serif heading “These shelves are bare.”, one sentence, and a “Clear filters” link button that resets everything.
  9. Each re-render fades icons in from 10px below at 94% scale, 500ms expo-out, staggered 18ms by position.
  10. Hovering or focusing an icon lifts it 8px and scales it to 1.08 over 320ms. Its drop shadow deepens. A dark name pill appears 10px above it.
  11. Clicking an icon opens a modal detail sheet: “Close Esc” at top left, previous and next round buttons at top right, the icon at 176px, the app name, a category pill, four detail rows (Studio, Released, Finish, Platform) and a four-swatch palette strip with hex keys.
  12. In the sheet, ArrowLeft and ArrowRight move through the currently filtered list and wrap at both ends. Esc, the Close button, or a click on the backdrop closes it.
  13. When the sheet closes, focus returns to the tile of the icon that was last shown.
  14. The shelf column count follows the available width: 8 at 1040px and up, 6 at 780px, 4 at 520px, 3 below. It re-chunks only when the count changes.

Structure

1280 × 800
┌──────────────────────────────────────────────────────────────────────────┐
│ Vitrine³⁶        [ ⌕ Search 36 icons            / ]   Collection Makers  │ 64px sticky, blur
│                                                       Notes (Submit an icon)
├──────────────────────────────────────────────────────────────────────────┤
│  max-width 1180, padding 0 40px                                          │
│  Small squares, kept with care.              Thirty-six app icons, ...   │ intro 36px top
│ ───────────────────────────────────────────────────────────────────────  │ 1px rule
│  (All 36)(Weather 3)(Health 6)(Productivity 6)(Music 4)(Photo 3)...       │ chips 32px
│  ● ● ● ● ● ● ● ● ●   [All|Flat|Glossy|Dark]              36 on display   │
│                                                                          │
│   ▢    ▢    ▢    ▢    ▢    ▢    ▢    ▢     84px tiles, 8 columns          │
│  ════════════════════════════════════════  15px glass plank, -16px      │
│                                             62px gap to next shelf       │
│   ▢    ▢    ▢    ▢    ▢    ▢    ▢    ▢                                    │
│  ════════════════════════════════════════                                │
└──────────────────────────────────────────────────────────────────────────┘

Detail dialog, 540px wide, radius 22
┌──────────────────────────────────────┐
│ Close Esc                    (‹) (›) │
│              ▢ 176px                 │
│            Ambergrain                │
│             (Photo)                  │
│ Studio                     Mabel Ito │
│ Released                    May 2023 │
│ Finish                        Glossy │
│ Platform               iPhone & iPad │
│ COLOUR PALETTE                       │
│ [████████|████████|████████|███████] │ 42px strip
│ ● #EE6A2C ● #AB4C20 ● #2A1A12 ● ...  │
└──────────────────────────────────────┘
  • Header is a header with the brand as text, a label wrapping the search input type="search", and a nav with three links plus one pill button.
  • Intro is a section with the only h1 and a p.
  • Filters are a section holding three role="group" containers with aria-label Category, Colour, Finish, plus a p with aria-live="polite" for the count.
  • The hall is a section; each shelf is a div grid; each icon is a button with an accessible name “Name, Category”.
  • The detail sheet is a native dialog opened with showModal(), labelled by its h2. The details are a dl.
  • A hidden zero-size svg holds the shared defs: the squircle clipPath, the light gradient, the rim gradient, the gloss gradient and the glyph shadow filter.

Tokens

:root {
  /* colour */
  --bg: #f4f2ee;        /* bone paper */
  --paper: #fbfaf7;     /* dialog, chips, pressed segment */
  --well: #ebe8e2;      /* search field, segmented track, pill */
  --ink: #1c1b19;
  --ink-2: #4f4c46;
  --ink-3: #6b675f;     /* meta text; 4.9:1 on --bg */
  --line: #e0dcd4;
  --accent: #d9480f;    /* the one vermilion */
  --focus: #d9480f;
  /* type */
  --serif: "Instrument Serif", Georgia, serif;
  --sans: "Onest", system-ui, sans-serif;
  --fs-display: 54px; --fs-empty: 34px; --fs-name: 24px;
  --fs-body: 15px; --fs-ui: 14px; --fs-chip: 13px; --fs-meta: 12px;
  /* space (4/8 base) */
  --s-1: 4px; --s-2: 8px; --s-3: 12px; --s-4: 16px; --s-5: 24px; --s-6: 40px; --s-7: 62px;
  /* shape */
  --tile: 84px;          /* 62px under 640px */
  --r-chip: 999px; --r-field: 10px; --r-seg: 9px; --r-dialog: 22px; --r-strip: 10px;
  /* shadow */
  --sh-tile: drop-shadow(0 6px 8px rgba(40,30,10,.16)) drop-shadow(0 1px 1px rgba(40,30,10,.12));
  --sh-tile-hover: drop-shadow(0 14px 16px rgba(40,30,10,.22)) drop-shadow(0 2px 2px rgba(40,30,10,.12));
  --sh-dialog: 0 30px 80px rgba(30,20,5,.28);
  /* motion */
  --ease: cubic-bezier(.16,1,.3,1);
  --std: cubic-bezier(.2,.7,.2,1);
  --t-lift: 320ms; --t-in: 500ms; --t-stagger: 18ms; --t-hint: 2600ms;
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
BrandInstrument Serif28px4001-0.01emTitle
Brand count (sup)Onest10px50010.04emNumerals
HeadlineInstrument Serif54px4001-0.02emSentence, one word in italic accent
Intro paragraphOnest15px4001.50Sentence, max 330px
Nav linksOnest14px4001.50Title
ChipOnest13px50010Title, count at 55% opacity
SegmentedOnest13px40010Title
CountOnest13px4001.50tabular-nums
Name pillOnest12px5001.20Title
Dialog nameOnest24px6001.2-0.01emTitle
Detail rowsOnest14px400 label / 500 value1.50Sentence
Palette headingOnest12px50010.04emUppercase
Empty headingInstrument Serif34px4001.10Sentence

The serif is only for the brand, the headline and the empty-state heading. Everything a user clicks is in Onest.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
Tile entryeach renderopacity, transform0, translateY(10px) scale(.94) → 1, none500ms, delay index × 18msexpo-outnone, tiles appear
Tile lifthover, focus-visibletransformnone → translateY(-8px) scale(1.08)320msexpo-outno lift; name pill still shows
Tile shadowhoverfilter—sh-tile → —sh-tile-hover320msexpo-outinstant
Tile pressactivetransform→ translateY(-4px) scale(1.02)320msexpo-outnone
Name pillhover, focus-visibleopacity, transform0, translate(-50%,4px) → 1, translate(-50%,0)150ms / 200msstd / expo-outinstant
Search hintevery 2.6sopacity, transform1 → 0, translateY(-6px), swap text, back250ms each waystdinterval never starts
Dialog openshowModalopacity, transform0, translateY(16px) scale(.97) → 1420msexpo-outinstant
Chip, swatchhoverbackground, scaleswatch 1 → 1.12150msstdinstant

Use animation-fill-mode: backwards on the tile entry, not both. With both the finished animation keeps owning transform and the hover lift stops transitioning.

States

  • Chip resting: paper fill, 1px --line border, --ink-2 text. Hover: border --ink-3, text --ink. Pressed: ink fill, paper text, aria-pressed="true".
  • Swatch resting: the family colour with a 1px inset 12% black ring. Hover: scale 1.12. Pressed: an extra 2px ink ring drawn with ::after at inset: -4px.
  • Segment pressed: paper fill, 0 1px 2px rgba(0,0,0,.08) shadow, ink text.
  • Search focused: paper fill, 1px --ink-3 border, the rotating hint is hidden. A typed value also hides the hint (:not(:placeholder-shown)).
  • Tile hover and focus-visible: lifted, deeper shadow, name pill visible.
  • Tile focus-visible: 2px vermilion outline, 3px offset, 8px radius, on top of the lift.
  • Empty: shelves replaced by the empty block, count reads “0 on display”.
  • Dialog nav button hover: --well fill.
  • Loading and error: not used. The collection is local data.

Accessibility

  • One h1 (the headline). The dialog name is an h2. The empty state heading is an h2.
  • Every icon is a button with aria-label="Saltmarsh, Weather". The SVG inside is aria-hidden="true". The name pill is aria-hidden because the label already carries the name.
  • Filter controls are buttons with aria-pressed. Swatches have spoken names (“Teal”, “Black and white”), never just a colour.
  • The count uses aria-live="polite" so filter results are announced.
  • / focuses search. It is ignored while the dialog is open or the field already has focus.
  • The dialog uses native showModal() for the focus trap and Esc. ArrowLeft and ArrowRight browse. Focus returns to the tile on close.
  • Contrast: #1c1b19 on #f4f2ee is 15.6:1. #6b675f on #f4f2ee is 4.9:1. Paper text on the ink chip is 16:1.
  • Hit targets: chips 32px tall with 6px gaps; swatches are 24px with a 6px gap. On touch layouts raise swatches to 32px.
  • The colour filter is never the only way in: typing “blue” in search finds the same icons.

Responsive rules

  • ≥1280: as specified, 8 columns, max-width 1180.
  • 1024–1279: 8 columns while the hall is at least 1040px wide, else 6. The intro stays two columns.
  • 900 and below: the nav links hide (the Submit button stays). The intro stacks: headline, then paragraph, 12px apart.
  • 768: 6 columns down to 780px of hall width, then 4.
  • Under 640: tile 62px, page padding 16px, headline 38px, the header loses the nav, the search field fills the row. The chip row scrolls sideways inside itself (overflow-x: auto, hidden scrollbar), never the page. 3 columns per shelf. Dialog padding 18px, big icon 136px.
  • At 375 the page has no horizontal scroll. Check document.documentElement.scrollWidth <= innerWidth.

Acceptance checklist

Always

  • Icons are drawn as vector art clipped to a continuous-corner squircle, never a copied raster icon.
  • Each shelf is a glass plank (translucent fill, backdrop blur, white top edge, soft drop) that sits in front of the icon reflections.
  • Category, colour and finish filters combine with AND, and a live count reports the result.
  • A no-results state offers a single “Clear filters” action.
  • Hover and keyboard focus both lift the icon and reveal its name.
  • The detail view is a modal dialog with Esc, backdrop close, previous/next and focus return.
  • The detail view shows the icon large, its metadata rows and a palette strip with hex keys.
  • Column count adapts to width (8 / 6 / 4 / 3) and the page never scrolls sideways at 375px.
  • Reduced motion removes the entry stagger, the lift and the rotating search hint.

This demo

  • Brand “Vitrine” with a superscript 36; headline “Small squares, kept with care.” with “kept” italic in #d9480f.
  • 36 icons in nine categories: Weather 3, Health 6, Productivity 6, Music 4, Photo 3, Money 4, Reading 3, Travel 5, Games 2.
  • Tiles are 84px; shelves have a 62px bottom gap and a 15px plank 16px below the tiles.
  • Selecting Blue shows “5 on display”; adding Dark shows “1 on display”; adding Reading shows the empty state.
  • Opening the ninth icon shows “Ambergrain”, category Photo, studio “Mabel Ito”, palette #EE6A2C #AB4C20 #2A1A12 #FFF3EA.

Implementation notes

1. One icon recipe for every tile. Each icon is the same five layers in a 100-unit viewBox: a flat plate in --b, a shared top-light gradient, an optional gloss ellipse, the glyph in --g with an accent in --a, and a 1.6-unit rim stroke whose gradient is white at the top and dark at the bottom. Only the three custom properties change per icon, so no gradient ids have to be unique.

<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
  <clipPath id="sq"><path d="M31 0H69C92 0 100 8 100 31V69C100 92 92 100 69 100H31C8 100 0 92 0 69V31C0 8 8 0 31 0Z"/></clipPath>
  <linearGradient id="lit" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0" stop-color="#fff" stop-opacity=".32"/><stop offset=".48" stop-color="#fff" stop-opacity="0"/>
    <stop offset="1" stop-color="#000" stop-opacity=".2"/></linearGradient>
  <filter id="gs"><feDropShadow dx="0" dy="2.2" stdDeviation="1.8" flood-opacity=".26"/></filter>
</defs></svg>

<svg class="ic glossy" viewBox="0 0 100 100" style="--b:#ee6a2c;--g:#fff3ea;--a:#2a1a12">
  <g clip-path="url(#sq)">
    <rect class="bg" width="100" height="100"/>
    <rect width="100" height="100" fill="url(#lit)"/>
    <g class="gl" filter="url(#gs)">…glyph…</g>
    <path d="M31 0H69C92 0 …Z" fill="none" stroke="url(#rim)" stroke-width="1.6"/>
  </g>
</svg>
.ic .bg { fill: var(--b); }
.ic .gl { fill: var(--g); }
.ic .gl .a { fill: var(--a); }
.ic .gl .s { fill: none; stroke: var(--g); stroke-width: 6; stroke-linecap: round; stroke-linejoin: round; }
.ic.flat .lit { opacity: .55; }

The squircle path has straight sides from 31 to 69 and corner curves whose 45° point sits about 6.9 units in. That is close to the platform’s continuous corner. A plain border-radius: 22% is an acceptable fallback, but it reads rounder and puffier.

2. Glass shelf in front of a reflection. The reflection belongs to the tile; the plank is the shelf’s ::after with a higher z-index and backdrop blur, so the reflection is softened behind it.

.tile { -webkit-box-reflect: below 3px linear-gradient(transparent 62%, rgba(0,0,0,.2)); }
.shelf { position: relative; display: grid; grid-template-columns: repeat(var(--cols), 1fr);
         justify-items: center; padding: 0 28px; margin-bottom: 62px; }
.shelf::after { content: ""; position: absolute; left: 0; right: 0; bottom: -16px; height: 15px;
  border-radius: 8px; z-index: 2;
  background: linear-gradient(180deg, rgba(255,255,255,.78), rgba(232,229,223,.62));
  backdrop-filter: blur(5px); border: 1px solid rgba(255,255,255,.9);
  box-shadow: 0 1px 0 rgba(0,0,0,.05), 0 14px 22px -12px rgba(60,40,10,.35); }

-webkit-box-reflect is supported in Chromium and Safari. In Firefox there is no reflection; the plank still reads. Do not fake the reflection with a flipped duplicate icon; it doubles the DOM and the hover state.

3. Chunk rows in JS, not with CSS wrap. A wrapping grid cannot draw one plank per row. Chunk the filtered list by the current column count, render one .shelf per chunk, and re-render on resize only when the count changes.

const cols = () => { const w = hall.clientWidth; return w >= 1040 ? 8 : w >= 780 ? 6 : w >= 520 ? 4 : 3; };
function render() {
  const c = cols(); let html = '';
  for (let r = 0; r < list.length; r += c)
    html += `<div class="shelf" style="--cols:${c}">` +
      list.slice(r, r + c).map((d, k) => tile(d, r + k)).join('') + '</div>';
  hall.innerHTML = html;
}
let last = 0; addEventListener('resize', () => { const c = cols(); if (c !== last) { last = c; render(); } });

Common mistakes:

  • Copying real app icons or tracing them. Every icon here is a fresh glyph on a flat plate.
  • Writing the app name inside the icon. Icons carry no words.
  • Rounded rectangles with a heavy 1px border instead of a squircle with a gradient rim.
  • A drop shadow on the shelf that is darker than the icons’ shadows. The plank is lighter than the page.
  • Filters as a select. They are visible pressed buttons so the state is readable at a glance.
  • Opening a new page for the detail view. The sheet is a dialog so the shelves stay in place behind it.
  • Forgetting focus return: keyboard users lose their place on the wall.
  • Letting the chip row push the page wider on phones.

Rebuild order:

  1. Put the shared defs in a hidden SVG and write the icon(d) function.
  2. Lay out header, intro and filter bar.
  3. Render shelves from the data with the chunking function.
  4. Add the glass plank and the reflection.
  5. Wire filters, count and empty state.
  6. Add the dialog with previous/next, Esc and focus return.
  7. Add hover lift, entry stagger, the rotating hint, then the reduced-motion block.

Details

Palette
Type
Instrument Serif · Onest
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Icon Museum

Let the icons be the only colour. Give each one a shelf, a lift on hover, and a quiet label.

All sources