Skip to content

IconPicker

IconPicker is a controlled icon property control. The closed control shows the selected glyph and its name. Pressing it opens a popout with a title, a search field, category and library filters, and a grid of bordered tiles, each with its glyph centered in a fixed area and its name always visible beneath it.

The picker is library-neutral. It browses exactly the catalog you hand it, never resolves icon packages, and emits a qualified identity (library, key and optional variant), never SVG, a CSS class, a display name or an index.

import { useState } from 'react';
import { IconPicker, type IconPickerCatalog, type IconPickerValue } from '@cratis/components/IconPicker';
export function ToolbarIconField({ catalog }: { catalog: IconPickerCatalog }) {
const [icon, setIcon] = useState<IconPickerValue | null>(null);
return <IconPicker value={icon} onChange={setIcon} catalog={catalog} aria-label='Toolbar icon' />;
}

Import the stylesheet once, either the aggregate @cratis/components/styles or the area sheet @cratis/components/IconPicker/styles.

A host adapts whatever icon source it has into an IconPickerCatalog:

import type { IconPickerCatalog } from '@cratis/components/IconPicker';
const catalog: IconPickerCatalog = {
libraries: [
{ id: 'example-glyphs', name: 'Example Glyphs', version: '2.0.1', attribution: 'Drawn for the examples' },
{ id: 'sample-symbols', name: 'Sample Symbols' },
],
icons: [
{
library: 'example-glyphs',
key: 'home',
name: 'Home',
categories: ['Places'],
tags: ['house', 'start'],
renderPreview: () => <ExampleHomeGlyph />,
},
{
library: 'sample-symbols',
key: 'home',
name: 'Home',
categories: ['Places'],
renderPreview: () => <SampleHomeSymbol />,
},
],
};
FieldMeaning
libraries[].idThe stable library identity. Each entry’s library refers to it.
libraries[].name, version, attributionShown to tell providers apart and to credit them at the foot of the popout.
icons[].library, key, variantThe qualified identity the picker emits. Together they are the icon’s identity; the display name is not.
icons[].nameThe label under the glyph, also matched by search.
icons[].categoriesThe groups the icon is listed under. An icon with none lands in an “Other” group.
icons[].tags, aliasesExtra words that find the icon.
icons[].deprecatedKeeps the icon selectable but flags it.
icons[].renderPreviewDraws the glyph. Called only once the tile nears the viewport.
status, errorready (the default), loading, or error with a message to show.

The field names library, key and variant are the same on IconPickerEntry and IconPickerValue, so a stored value can be compared with, and found in, the catalog directly. Equality is all three fields: the same home key in another library, or in another variant, is a different icon.

Two libraries may define the same name. The tiles then name their provider, and a library filter appears, so duplicates stay distinguishable.

value is the selected qualified identity, or null. onChange receives a fresh { library, key, variant? } and is the only thing the picker emits. Choosing an icon calls onChange, closes the popout and returns focus to the trigger. Escape, the close button and pressing outside dismiss the popout without calling onChange.

Search and filters start fresh each time the popout opens; the selection never changes unless you change value.

PropEffect
readOnlyShows the selection. The control stays focusable, does not open, and never emits.
disabledDisables the control.
allowedA list of qualified values or a predicate over entries. Other icons stay listed, marked “Not available for this field”, and cannot be selected or emitted.
invalid, validationMessageMarks the control invalid; the message is shown under it and described to assistive technology.

The host enforces the effective exposure: pass the catalog it wants offered, and allowed for a narrower set within it.

A selection the catalog does not hold is never replaced by another icon. The trigger shows its raw qualified identity (for example retired-library / home) with a warning mark and a message, and the popout selects nothing.

While status is loading, the icons already supplied stay browsable and the current selection stays stable; a selection not yet loaded is shown by identity without a warning. When status is error, the popout shows error (or the error label) as an alert, still listing whatever was supplied. An empty catalog and a search with no matches each have their own message, and no-results offers a button to clear the filters.

The search index is built once per catalog, and typing only filters it. A tile calls renderPreview only once it comes near the viewport and keeps the result, so a library of thousands of icons costs nothing until it is scrolled. Browsing lists compact category groups of compactGroupSize icons (12 by default) with a Show all action; searching or choosing a category lists every match in one grid.

The popout opens beside the trigger at placement ('bottom start' by default), flips and shifts to stay within the viewport, and renders in a portal so a scrolling or clipping inspector cannot cut it off. On a viewport at most 30rem wide it becomes a sheet contained in the viewport.

Every label has an English default and can be replaced through labels, including the title, the search, the category and library filters, each state, and the summary sentence.

  • The trigger’s accessible name is aria-label (or the trigger label) followed by the selected icon’s name. With aria-labelledby, the selected name is appended to the referenced label.
  • Opening focuses the search field; Down moves into the icons. Each grid of tiles is a listbox with one Tab stop: the arrow keys move a tile or a row, and Home and End jump to the ends. Enter or Space selects.
  • The category filter is a group of toggle buttons with one Tab stop and arrow-key movement. The summary line announces what is showing.
  • Tiles, the close button and the filters meet the touch-target size. The selected tile has a check mark and a heavier border, the focused tile an offset outline, and a tile the field does not accept a dashed border and text, so none depends on color alone. Long or localized names wrap within the tile.

Style the stable parts through pt or the data-cratis-part attributes: root, trigger, glyph, name, message, popover, dialog, title, close, search, clear, categories, category, library, summary, status, group, groupTitle, showAll, grid, tile, tileIcon, tileName, provider and attribution.

The root and trigger carry data-disabled, data-invalid, data-readonly and data-open; the trigger also carries data-missing for a selection the catalog lacks. The active category and the selected tile carry data-selected, an unavailable tile carries data-disabled, and the status carries data-loading while the catalog loads. Colors come from the --cratis-* tokens.