@cf/ui

Iconography

Phosphor icons, sized by the component they sit in rather than by the icon.

Every icon in the top row gets its size from the Button around it through data-icon. The row underneath sets size-* on the icon directly, which is what you'd do outside a component.

Library

The icon set is Phosphor. components.json sets iconLibrary to phosphor, and every component that draws an icon imports it from @phosphor-icons/react: XIcon in the dialog close button, CaretDownIcon and CheckIcon in select, CaretRightIcon in dropdown submenus, ArrowLeftIcon and ArrowRightIcon in the carousel, CircleNotchIcon for Spinner. I use the default weight throughout; nothing in the components sets weight.

Sizing comes from the parent

An icon doesn't decide its own size. The component it sits in does, with a descendant selector:

  • Button sizes anything carrying data-icon: [&_[data-icon]]:size-4 [&_[data-icon]]:shrink-0. Plain svgs without the attribute are left alone.
  • Marker, MarkerIcon and InputGroup use [&_svg:not([class*='size-'])]:size-4, so an explicit size-* on the icon still wins.
  • Select sets [&_svg]:size-4 on its trigger and items. AlertDialogMedia uses the same :not([class*='size-']) pattern at size-6.

data-icon is currently a presence check. The carousel passes data-icon="inline-start" and the dialog passes a bare data-icon, and Button treats them the same: there is no inline-start or inline-end spacing rule yet. If I add one, the values will be inline-start and inline-end.

Colour

Icons use currentColor, so they follow the text token of whatever they're in. Dimming is done with opacity, as in the select trigger's opacity-50 caret, rather than with a separate icon colour.

Meaning

Decorative icons next to text need nothing extra. MarkerIcon sets aria-hidden="true". Icon-only controls always get a label: the dialog close button has <span className="sr-only">Close</span>, the carousel buttons say "Previous slide" and "Next slide", and Spinner renders with role="status" and aria-label="Loading".

Rules

The same rules ship in DESIGN.md and the MCP server, for the agents building with this.

  • must

    Use icons from @phosphor-icons/react, imported by their Icon-suffixed names such as XIcon and CaretDownIcon.

    use-phosphor

  • should

    Put data-icon on an icon inside a Button so the button's [&_[data-icon]]:size-4 [&_[data-icon]]:shrink-0 rule sizes it.

    mark-icons-in-buttons

  • should

    Leave out a size-* class on an icon unless you need a different size; containers like Marker and InputGroup apply size-4 to any svg without one.

    let-the-parent-size

  • must

    Give every icon-only control a text label, as a <span className="sr-only"> or aria-label, because the icon itself carries no name.

    label-icon-only-controls