Ability icons
NewA game HUD’s ability kit: bold coloured glyphs, an equipped slot with a 48-tick cooldown ring, and a ready flash.
ability-icons/ability-icons.tsxThe 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 game HUD with a small kit of abilities, one equipped at a time, that the player swaps between and fires.
- Abilities with cooldowns, where the player has to read without looking whether the equipped one can be used again.
- A kit whose pieces are told apart by colour first and by shape second: each glyph keeps one colour on any backdrop.
- Showing that some abilities have been upgraded, with a badge that pops in when the upgrade lands.
- Choosing the ability with a d-pad, the arrow keys or a tap, and firing it from a button or Enter.
When not to use
- An inventory of many items with counts. Use a grid of slots, or a quick selector wheel for a handful.
- A single timed effect with no choice to make. Use an effect timer.
- A resource that drains and refills while it is used, like stamina or heat. Use a stamina wheel or a heat gauge.
- A toolbar in an app. Use a segmented control or icon buttons with tooltips; the dark HUD plates belong in a game.
- Showing which button fires what. Use button hints or controller glyphs.
You can add this component with the shadcn CLI or by hand:
Into components/ability-icons of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/ability-icons.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
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
import { AbilityIcons } from './ability-icons';export function Hud() { return ( <AbilityIcons defaultSelected="lock" upgraded={['round-bomb', 'lock']} cooldowns={{ lock: 12 }} onUse={(ability) => console.log('used', ability)} autoplay={false} /> );}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.
The tool abilities of an adventure game, each a flat glyph in its own colour so the player knows it before its shape. Equip one from the strip and use it: the slot’s ring goes dark and lights again tick by tick, clockwise; trying too early shakes the slot, and the last tick flashes it ready. Upgraded abilities wear a cream “+”.
- Seven glyphs (round bomb, square bomb, magnet, time lock, ice block, camera, motorbike) are flat SVG silhouettes on a 54-unit square. Their details are cut out with a mask, never painted on, so the plate shows through them. An upgrade adds a cream “+” with a 2.5-unit moat cut around it.
- The equipped slot sits in a ring of 48 ticks, 3 units wide from radius 33 to 50 of a 100-unit box. Using an ability with a cooldown darkens every tick at once; they light again one by one from 12 o’clock, clockwise.
- Cooldowns are kept as a start time and a length, so they run on the clock, not on frames. A requestAnimationFrame loop only turns the elapsed time into a tick count, runs while something cools and the HUD is on screen, and sets state only when a count changes: 48 times per cooldown.
- The strip shows the same progress for every cooling ability in a small ring of its own: the same 48 ticks in a thinner SVG band, lit by the same count.
- The moments a player feels are Web Animations on transforms, opacity and filters: the use kick (a squash, then a punch), the shake of an ability used too early, the white flash and swell of the ring when it is ready again, the pop of a “+”. Under reduced motion they are skipped; the ring still counts.
- While on screen and untouched, the HUD plays a round of play: bombs thrown, one tried too early, abilities equipped by walking the strip like a d-pad, an upgrade. Any input of the reader’s ends it.
- The HUD sits on its own, centred on a plain stage (white, or near-black at night: light-dark() tokens following the page’s color-scheme or the colorScheme prop). Like a game’s HUD it brings its own dark plates, behind the strip, the name, the slot and the Use button, so the glyphs, the cream words and the ring’s dark ticks read the same on either; on the light stage the plates cast a soft shadow.
- Everything is vector and drawn at the screen’s resolution: SVG glyphs and tick rings, CSS plates. No shape is blurred or scaled as a bitmap; glows only ever sit behind the sharp shapes.
| Prop | Type | Default | Description |
|---|---|---|---|
abilities | AbilityId[] | all seven | The abilities the strip lists, in order: 'round-bomb', 'square-bomb', 'magnet', 'lock', 'ice', 'camera', 'bike'. |
defaultSelected | AbilityId | the first | The ability equipped at first. |
selected | AbilityId | — | The equipped ability, controlled; pair it with onSelectedChange. |
onSelectedChange | (ability: AbilityId) => void | — | Called when another ability is equipped. |
upgraded | AbilityId[] | ['round-bomb', 'square-bomb', 'lock'] | The abilities that wear the “+” of an upgrade. |
cooldowns | Partial<Record<AbilityId, number>> | — | Cooldowns in seconds, overriding each ability’s own (bombs 4, lock 8, ice 3, bike 6, magnet and camera none); 0 for none. |
onUse | (ability: AbilityId) => void | — | Called when the equipped ability is used; not when it is refused because it is still cooling. |
defaultCooling | Partial<Record<AbilityId, number>> | — | Abilities already cooling at first, each with the share of its cooldown that has passed (0 to 1); the ring counts on from there. Turns the demo off. |
autoplay | boolean | true | While on screen and untouched, the HUD plays a short round of play. Any input hands it to the reader. |
colorScheme | 'light' | 'dark' | 'system' | — | A light or dark stage, or the reader’s; without it, the page’s color-scheme. |
className | string | — | Added to the stage. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useEffectEvent, useRef, useState, type KeyboardEvent as ReactKeyboardEvent,} from 'react';import { ABILITIES, ABILITY_IDS, AbilityGlyph, type AbilityId } from './glyphs';import { useAutoplay } from './use-autoplay';import styles from './ability-icons.module.css';export { AbilityGlyph, ABILITIES, ABILITY_IDS, type AbilityId } from './glyphs';/* * Ability icons: the tool abilities of a game HUD, shown on their own on a plain stage. A strip * lists the kit, one glyph per ability in its own colour; the equipped one also sits in a slot * inside a ring of 48 ticks, which is how the player reads, without looking, whether it can be * used again. Like a game's HUD it brings its own dark plates, so it reads on any backdrop. * * - Using an ability kicks its glyph (a squash, then a punch) and, if it has a cooldown, darkens// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useId, type ReactNode } from 'react';/* * The ability glyphs: flat, bold silhouettes, one colour each, drawn on a 54-unit square (the * motorbike is wider and spills 5 units out each side, so every glyph sits in the same 64 × 60 * box). Details are cut out of the shape, never painted over it, so whatever is behind shows * through them: each glyph is a filled group under a mask whose black shapes are the cuts. * * An upgraded glyph gets a cream "+" in its lower right corner, with a 2.5-unit moat cut around it * so the badge reads on top of any colour. */export type AbilityId = 'round-bomb' | 'square-bomb' | 'magnet' | 'lock' | 'ice' | 'camera' | 'bike';export const ABILITY_IDS: AbilityId[] = [ 'round-bomb', 'square-bomb', 'magnet', 'lock',// Copyright (c) 2026 Bruno Delorence. MIT License.'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(/* Ability icons (ability-icons.tsx): the HUD on its own, centred on the lab's plain stage, white by day and near-black at night: the ability strip on top, the equipped ability's name under it, then the equipped slot with its cooldown ring and the Use button. Sizes are in units of the drawing, `--u` for the strip and `--s` for the slot: one unit is 1px when the stage is wide enough, and both shrink with the stage's width (a container query), so the strip always fits a 320px phone. Colours are the tokens below, 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 (data-color-scheme). As in a game, the HUD brings its own dark plates (the strip, the name's pill, the slot's disc, the Use pill), so the glyphs, the cream words and the ring's dark ticks read the same on a white stage and a black one; on the light stage the plates also cast a soft shadow so they sit on it. Crispness. Everything is vector: SVG glyphs and rings, CSS plates. No shape is blurred or scaled as a bitmap; the only soft things are glows and shadows, painted behind the sharp shapes. Memory. The stage is one layout boundary sized from outside. The rings' ticks change only an attribute, 48 times per cooldown; everything that moves does it by transform, opacity or filter. */.root { /* The stage, and its edge. */- The strip is a radiogroup: one Tab stop, the arrow keys, Home and End equip an ability, and Enter uses the equipped one. Each radio is named with its ability, its upgrade and whether it is cooling.
- Use is a real button named with the ability it fires; while that ability cools it is marked aria-disabled but still answers, with the shake and the time left.
- A polite live region says what happens: equipped, used and ready in how long, not ready yet, ready, upgraded.
- The glyphs, the slot and its ring are decorative (aria-hidden): every fact they show is in the names and the live region. The exported AbilityGlyph takes a title when a glyph should be announced on its own.
- Reduced motion turns off the demo and every kick, shake, flash and pop; the rings keep counting. Focus is a cream ring on the strip and the button.