Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Glossy app icon

New

Hyper-real app icons drawn in SVG and CSS: rich materials, raised piping, glass that catches the light as the icon tilts.

glossy-app-icon/glossy-app-icon.tsx
Live · interactive

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A hero moment for one app or product: a landing page, a launch post, an about screen, where the icon itself is the thing to look at.
  • An icon that should feel touchable on a desktop page: it leans towards the pointer and sinks when pressed, like a physical key.
  • A brand mark drawn at any size from one drawing, with no image file to export, host or swap for retina screens.
  • A small gallery of a few icons to compare, with the dock to switch between them on one stage.

When not to use

  • A grid of many app icons, a launcher or a settings list. Use flat images or plain SVG icons; each of these carries layered filters and shadows.
  • Icons inside buttons, menus or toolbars. Use a line icon set at 16 to 24 px.
  • The real icon of a shipped app in a store listing. Use the exported PNG the store asks for.
  • A page that has to stay quiet and flat. The deep shadow and the gloss draw the eye; use a simple tile with a logo.
  • Touch-only screens where the tilt is the point: a tap only presses it. Use a short entrance animation instead.

You can add this component with the shadcn CLI or by hand:

Into components/glossy-app-icon of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/glossy-app-icon.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

launcher.tsx
import { GlossyAppIcon } from './glossy-app-icon';export function Launcher() {  return (    <GlossyAppIcon      stage={false}      variant="aperture"      size={96}      onPress={(variant) => console.log('open', variant)}    />  );}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

Four app icons in the glossy, tactile style: a masked hero’s goggle on cobalt twill, a camera lens on pebbled leather, a brushed-metal dial and a stitched canvas patch. Each sits on a pale grid of blank tiles with a deep soft shadow; hover tilts it towards the pointer while the gloss and the reflections in the glass drift across it, and a press sinks it and lets it spring back. No image files: every icon is one SVG drawing lit by CSS layers.

  • Each icon is one 100-unit SVG drawing: the material as gradients, its weave as an SVG pattern (a diagonal twill, a canvas weave, satin stitches) or as SVG noise (pebbled leather, brushed metal), and every raised line as four stacked strokes: a blurred cast shadow, the tube, its highlight and a fine glint.
  • The dial’s knob is spun metal: two conic gradients under a fine repeating-conic hairline, in an HTML layer over the drawing so its highlights stay put while its orange mark sits at the level.
  • HTML layers light the drawing: a bevel of inset shadows (a lit top rim, a shaded bottom, a fine dark edge), a soft-light gloss from the top left, and a drop shadow of three layers that stays flat on the stage.
  • Hover tilts the icon up to 11 degrees towards the pointer. The pointer’s place goes into two custom properties, --tx and --ty, and the tilt, the gloss, the shadow, the knob and the reflections inside the glass all read them, each moving by a transform at its own depth, so the reflections slide across the lens as it turns. Nothing is redrawn.
  • A press scales the face down 4.5% in 110 ms, darkens it and tightens the shadow; on release it springs back in 460 ms with an overshoot.
  • The stage sizes everything from one length, --s: the icon is 61% of the stage’s shorter side, and the blank tiles are the same size, 12% of it apart, fading out at the far left and right.
  • While on screen and untouched, the stage tilts the icon along a slow path, presses it, and moves on to the next icon; any pointer or key hands it to the reader, and it takes over again 5 seconds later.
PropTypeDefaultDescription
variant'visor' | 'aperture' | 'tune' | 'patch''visor'Which icon: a masked hero’s goggle, a camera lens, a brushed-metal dial or a stitched patch. On the stage, the one it starts on.
sizenumber—The icon’s side in px. Without it, on the stage it takes 61% of the stage’s shorter side; alone, 120.
stagebooleantrueDraws the stage: the grid of blank tiles and the dock of icons to pick from. Without it, just the icon.
labelstring—The icon’s accessible name; its own name by default.
onPress(variant) => void—Called when the icon is pressed (click, tap, Enter or Space).
tiltbooleantrueHover tilts the icon towards the pointer, and the reflections move with it.
autoplaybooleantrueOn the stage, while on screen and untouched, it tilts itself, presses and moves through the icons. Any input stops it.
colorScheme'light' | 'dark' | 'system'—Light or dark stage, or the reader’s; without it, the page’s color-scheme. The icons look the same on both.
defaultTilt{ x: number; y: number }—Opens tilted towards this point (-1 to 1 on each axis), as if the pointer rested there. Turns the demo off.
defaultPressedbooleanfalseOpens pressed in, as if held down. Turns the demo off.
classNamestring—Classes for the root.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

glossy-app-icon.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useEffect,  useId,  useRef,  useState,  type CSSProperties,  type KeyboardEvent as ReactKeyboardEvent,  type PointerEvent as ReactPointerEvent,  type ReactNode,} from 'react';import { useAutoplay } from './use-autoplay';import styles from './glossy-app-icon.module.css';/* * Glossy app icon: a hyper-real app icon, drawn in SVG and CSS with no image file. A squircle of * rich material (twill fabric, pebbled leather, brushed metal, canvas) with raised piping, a thick * dark bezel round a glass lens, a lit rim and a deep soft shadow, on a pale grid of blank tiles. * * - Every icon is one 100-unit SVG drawing (the material, its weave as an SVG pattern, the raised *   lines as stacked strokes: a blurred cast shadow, the tube, its highlight) inside HTML layers
glossy-app-icon.module.css
/* Glossy app icon (glossy-app-icon.tsx): the icon, and the stage it sits on — a pale grid of blank   tiles the icon's size, 12% of it apart, with the icon in the middle at 61% of the stage's   shorter side.   Every length of the icon is a share of --s, its side, so one drawing serves every size: the   bevel, the gloss and the shadow scale with it, and the SVG inside is 100 units wide.   The pointer's place over the icon is --tx and --ty (-1 to 1), set from the script; the tilt,   the gloss, the shadow and the reflections in the glass all read them, and all ease over   --ease-ms. Only transforms and opacity change, so a hover never repaints the drawing.   Colours are the stage's tokens, light-dark(light, dark) where the two differ: the component   takes the color-scheme it inherits from the page, or the one its colorScheme prop sets. The   icons themselves look the same on both. */.root,.bare {  /* The stage and its tiles. */  --bg: #fdfdfd;  --tile: #e5e5e5;  --tile-2: #eaeaea;  /* The icon's drop shadow. */  --drop: rgb(0 0 0 / 0.05);  --drop-mid: rgb(0 0 0 / 0.24);
use-autoplay.ts
'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(      ([entry]) => {        onScreen = entry.isIntersecting;
  • The icon is a real button named after it (“Visor, app icon”); Enter and Space press it, with the same sink and spring as a click, and a polite live region says it is opening.
  • The dock is a radio group: the chosen icon is the one tab stop, the arrow keys move between them, and the choice is announced.
  • All the drawing is decorative and hidden from screen readers; the focus ring sits outside the squircle.
  • With reduced motion the icon never tilts or pops in and the demo does not play; the gloss still answers the pointer and a press still shows.
  • The demo pauses off screen and in a hidden tab.