Icon
StableOne component for all 161 icons: a 24-pixel grid, a 1.5 stroke, square ends, coloured by the text around it.
Icon draws any icon in the set by name. It is decorative by default and hidden from screen readers; give it a title when it stands alone and means something. Size comes from a size-* class or the size prop, colour from the text colour, and every other SVG attribute passes through.
Sizes
import { Icon, iconMeta, type IconName } from '@rchursin/product/icons/Icon'import { Link } from '@rchursin/product/lib/router'
/** * One drawing, scaled. The 1.5 stroke on the 24 grid paints 1px at 16, 1.25 at 20, 1.5 at 24 * and 2 at 32. Size comes from a size-* class (or the size prop); colour from the text colour. */const names: IconName[] = ['calendar', 'agent', 'session', 'status-progress', 'priority-high']
const sizes = [ { px: 16, className: 'size-4', stroke: '1' }, { px: 20, className: 'size-5', stroke: '1.25' }, { px: 24, className: 'size-6', stroke: '1.5' }, { px: 32, className: 'size-8', stroke: '2' },]
export default function IconSizes() { return ( <div className="flex w-full max-w-md flex-col gap-6"> <ul className="flex flex-col divide-y divide-line border-y border-line">Usage
import { Icon, iconMeta, type IconName } from '@rchursin/product/icons/Icon'<Icon name="calendar" className="size-4" /> {/* decorative: the text beside it says it */}<Icon name="alert" title="Overdue" className="size-4" /> {/* stands alone: role="img" with a name */}Examples
Decorative or meaningful
The calendar repeats the date beside it, so it stays hidden. Nothing else on the row says “Overdue”, so that icon gets a title.
import { Icon } from '@rchursin/product/icons/Icon'import { SampleBadge } from '@rchursin/product/shell/layout'
/** * Decorative or meaningful. The calendar sits next to a date that already says it, so it stays * hidden from screen readers (the default). Nothing else on the row says "Overdue", so that * icon gets a title: it becomes role="img" with its own name. */const tasks = [ { id: 'RC-131', title: 'Rotate the staging API keys', due: 'Oct 9', overdue: true }, { id: 'RC-134', title: 'Merge api-docs into openapi-writer', due: 'Oct 21', overdue: false }, { id: 'RC-136', title: 'Rewrite the ui-snapshot description', due: 'Oct 24', overdue: false },]
export default function IconMeaning() { return ( <div className="flex w-full max-w-md flex-col gap-4"> <div className="lux-card overflow-hidden rounded-2xl bg-raised"> <div className="flex items-center justify-between gap-3 border-b border-line px-4 py-3"> <h3 className="text-[14px] font-semibold tracking-[-0.01em] text-fg">Due this month</h3>In buttons
Button and IconButton take an icon by name. In your own controls, put Icon next to the words.
import { Icon } from '@rchursin/product/icons/Icon'import { Button, IconButton } from '@rchursin/product/ui'
/** * Button and IconButton take an icon by name and size it for you (16px in a 32px button). * In your own controls, put <Icon> beside the words with a size-* class. With no words, * name the control, never the picture. */export default function IconInButtons() { return ( <div className="flex w-full max-w-lg flex-col items-center gap-6"> <div className="flex flex-wrap items-center justify-center gap-3"> <Button icon="plus">New task</Button> <Button variant="secondary" icon="download"> Export </Button> <Button variant="outline" iconRight="arrow-right"> Open board </Button> </div> <div className="flex flex-wrap items-center justify-center gap-3"> <div role="group" aria-label="Task actions" className="flex items-center gap-1 rounded-xl p-1 ring-1 ring-line ring-inset"> <IconButton icon="pencil" label="Edit task" /> <IconButton icon="copy" label="Duplicate task" /> <IconButton icon="archive" label="Archive task" /> </div> <button type="button" className="lux-btn lux-btn-ghost inline-flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-[13px] font-medium text-fg-2 hover:text-fg"> <Icon name="filter" className="size-4" /> Filters <span className="font-mono text-[11px] text-fg-3 tabular-nums">2</span> <span className="sr-only"> active</span> </button> </div> </div> )}The set as data
iconMeta lists every icon with its category and keywords, for your own pickers and indexes.
import { Icon, iconMeta } from '@rchursin/product/icons/Icon'
/** * iconMeta lists every icon with its category and search keywords, so you can build your own * pickers and indexes from it. Here: the Product category, the icons drawn for this product. */const product = iconMeta.filter((m) => m.category === 'Product')
export default function IconMetadata() { return ( <div className="flex w-full max-w-xl flex-col gap-3"> <p className="font-mono text-[11px] tracking-[0.12em] text-fg-3 uppercase"> Product · {product.length} of {iconMeta.length} icons </p> <ul className="grid grid-cols-2 gap-px overflow-hidden rounded-2xl bg-line ring-1 ring-line sm:grid-cols-3"> {product.map((m) => ( <li key={m.name} className="flex min-w-0 items-center gap-3 bg-raised px-3.5 py-3 last:col-span-2"> <Icon name={m.name} className="size-5 shrink-0 text-fg" /> <span className="flex min-w-0 flex-col"> <span className="truncate font-mono text-[12px] text-fg">{m.name}</span> <span className="truncate text-[11.5px] text-fg-3">{m.keywords.join(', ')}</span> </span> </li> ))} </ul> </div> )}API
Icon props
| Prop | Type | Default | Description |
|---|---|---|---|
namerequired | IconName | – | Which icon. The type lists all 161 names, so a typo is a compile error. |
size | number | string | 24 | Width and height in pixels (or any CSS length). A size-* class on className wins over it. |
title | string | – | Gives the icon its own accessible name: it renders a <title> and role="img". Leave it out when nearby text says the same thing. |
strokeWidth | number | string | 1.5 | The set is drawn for 1.5 on its 24 grid. Change it only for a deliberate exception. |
iconMeta entries
| Prop | Type | Default | Description |
|---|---|---|---|
namerequired | IconName | – | The icon’s name, in kebab-case, for what it shows (trash, not delete). |
categoryrequired | string | – | One of ten: Navigation, Actions, Status, Time, Notes, People, Data, Analytics, Product, Brands. |
keywordsrequired | string[] | – | What people search for when they need it (“delete”, “remove” for trash). |
Accessibility
- Without a name the SVG gets aria-hidden="true" and focusable="false", so screen readers skip it and Tab never lands on it.
- A title (or an aria-label or aria-labelledby you pass) switches it to role="img" with that name. A title also shows as a tooltip on hover.
- In an icon-only button, name the button (IconButton’s label), not the icon: names on SVGs inside buttons are read unreliably.
- The stroke is currentColor, so the icon has the same contrast as its text. Every text token, fg-3 included, clears the 3:1 that WCAG 1.4.11 asks of icons that carry meaning.
Guidelines
Do
- Match the text: size-4 (16 pixels) beside 13–14 pixel text and in 32 pixel buttons, size-3.5 in 28 pixel buttons.
- Say it in words too. An icon supports a label; it rarely replaces one.
- Let it take the text colour, and change the text colour if the icon needs to be quieter.
Don’t
- Don’t change the stroke width, caps or joins: the set is drawn for 1.5, square and mitred.
- Don’t put a title on an icon whose label is already beside it; it is read twice.
- Don’t use colour or opacity to give an icon a meaning.
In use
Used on this site
Related
- Icon browserSearch all 161 icons by name or use, check them at 16 to 24 pixels, and copy one as SVG, JSX or its name.
- Icon guidelinesHow the icons are built, how status and priority are drawn, how to use icons, and how the set ships, as live sections.
- Icon buttonA square button with just an icon. The label is required: it is the accessible name and the tooltip.
- ButtonThe lit-metal button in five finishes and five sizes, with icons, a loading state and shortcut hints.
- Status & priorityTask glyphs told apart by shape: a square that fills as work moves on, priority bars, assignee, labels and estimate.
- Screen iconsFifteen small round-capped icons used by the library-style screens, each its own component.
Source
The component’s own files, exactly as they run on this site.
/** * RChursin_ icons: 24 × 24 grid, 1.5 stroke, square caps, mitred corners. * * <Icon name="calendar" className="size-4" /> decorative (aria-hidden) * <Icon name="alert" title="Overdue" /> standalone meaning (role="img" + <title>) * * Size comes from `size` or a Tailwind size class; colour from the text colour. * In an icon-only button, put the name on the button (aria-label or hidden text), not on the icon. */import { createElement, type SVGProps } from "react"import { iconNodes, type IconName } from "./data"import type { IconElement } from "./types"
export type { IconName }
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, "name" | "ref" | "children"> { name: IconName /** Pixel size (or any CSS length). A size-* class on className overrides it. */ size?: number | string /** Gives the icon its own accessible name. Leave out when the text next to it says the same thing. */