Icon sets

Experimental. Ship an icon set: register each icon against an icon definition, so apps find icons by meaning and templates use your icons.

Experimental

Icon sets are experimental: the npx @astryxdesign/cli experimental icon commands, the experimental.iconSet key in astryx.config, the experimental.iconSets key in an integration manifest, the data file, icon definitions, and the output and JSON of all of them can change, or go away, in any patch release without a deprecation period. A project that does not set experimental.iconSet sees no change in any stable command, apart from: the marked experimental group in help and in npx @astryxdesign/cli manifest (its command, and its responseTypes and jsonSupported entries); the marked experimental entries in the CLI reference listings; and a search for "experimental", which also finds the stable stability field of the field reference. Templates, discover, and doctor answer as they do with no icon set installed. With a package that ships icon sets installed and no opt-in, the warnings an older CLI gives about the manifest key experimental (in npx @astryxdesign/cli doctor and npx @astryxdesign/cli doctor integration validate <package>, and the one-line nudge other commands print to stderr) are gone. npx @astryxdesign/cli search never returns icons or these docs; to find an icon by meaning, use npx @astryxdesign/cli experimental icon <words>.

How it works

Astryx keeps a list of icon definitions: one file per meaning, like close or delete (npx @astryxdesign/cli docs cli/experimental/icon-definitions). An icon set registers which of its icons draws each definition, in the map of its data file. Templates say which definition each of their icons draws, so a project that uses your set gets your icon for every definition you map. Every set maps the built-in definitions, the names Core renders itself.

The CLI knows no icon library. You, or your agent, pick each icon from the library you wrap; the commands below verify it, write the files, and check the set.

Add an icon set

bash
npx @astryxdesign/cli experimental icon new-set outline

outline.doc.mjs is the descriptor: name, display name, description, and license, the SPDX license of the artwork (it starts as your package's own). outline.icons.json starts with no icons and an empty map; its module is your package name. outline.generated.js is the set's generated module, with its declaration file outline.generated.d.ts: the CLI writes both, and every icon and the theme map are exports of it. Every field is in npx @astryxdesign/cli docs cli/experimental/icon-set-files icon-set-doc and npx @astryxdesign/cli docs cli/experimental/icon-set-files icon-set-data.

Add icons one at a time

Pick an icon from the library and ask which definitions fit it. Nothing is written yet:

bash
npx @astryxdesign/cli experimental icon add Bin --from @acme/glyph-library

Each row is an Astryx definition (or one your set declares) with the exact command that adds the icon for it. Run one:

bash
npx @astryxdesign/cli experimental icon add Bin --from @acme/glyph-library --definition delete

The CLI reads the library's exports without running them and refuses a name the module does not export, suggesting close names. It adds the icon as BinIcon (id bin, words from its name), writes export {Bin as BinIcon} from '@acme/glyph-library'; to the set's generated module, and sets map.delete = "bin". Running it again changes nothing. Do not copy the definition's words into the icon: npx @astryxdesign/cli experimental icon matches an icon by its own words and by the name and words of every definition it is mapped to.

When no Astryx definition fits, add your own in the same command. It needs no pull request:

bash
npx @astryxdesign/cli experimental icon add RotateCw --from @acme/glyph-library --new-definition rotate --description 'Rotate an image or shape.' --words turn,spin

experimental icon map <definition> <icon> points a definition at an icon the set already has, and --remove drops an entry. experimental icon new-definition <name> --description ... --words ... --icon <id> adds your own definition for an icon you already have.

Or wrap a whole library

bash
npx @astryxdesign/cli experimental icon new-set outline --from @acme/glyph-library

Every component the library exports becomes an icon (an alias of one component is recorded once), named <Name>Icon, and map is filled only where a definition's name or one of its words is an icon's id. Run it again after a library release to refresh the icons; map entries whose icon still exists are kept, and the receipt names any that lost theirs.

A library whose icons are data (objects or SVG strings) has no components to find: create the set, declare an adapter in its data file (see library shapes), and run the command again; every data export then becomes an icon through the adapter. When every icon export shares a leading word, such as feather in featherEye and featherSun, ids and export names leave it out: featherEye becomes id eye, exported as EyeIcon. The prefix is read over icon-like exports only: SCREAMING_CASE constants (README, VERSION), number and boolean exports, and, next to components or icon objects, plain strings are never icons. Leave out any other export that is not an icon with --exclude meta,version. experimental icon add reads the prefix the same way, and reuses the icon an export already has, so its commands need no --id.

How icons are exported

Every icon is a named export of your package's public entry, so apps, templates, and theme maps all import components from one place:

  • The data file's module is your package name or one of its package.json exports subpaths, never a file path. integration check fails otherwise.
  • A variant is a named look the set supplies (for example outline or solid); size comes from CSS. List the names in variants in the data file and the one each icon's own export draws in default.
  • Each export is named <Name>Icon, and a variant <Name><Variant>Icon, such as TrashSolidIcon. experimental icon add names them that way; integration check warns on another name.
  • Each export is a React component: a function, a class, forwardRef or memo, or what your adapter returns. integration check fails on a plain object or string.
  • It accepts SVG props (className, style, aria-*, width, height), draws with currentColor, and fills its box (1em or 100%), so <Icon icon={TrashIcon} size="sm" /> sizes and colors it like a built-in icon. The CLI cannot check this statically; the test below can.

experimental icon add writes the set's generated module (generated in the data file) and its declaration file, which re-exports each component's own types and types each adapted icon as ComponentType<SVGProps<SVGSVGElement>>. Add one line to the file your package's entry points at, and to its types; the receipt names it:

js
export * from './icon-sets/outline/outline.generated.js';

An optional render test you can copy:

js
import {renderToStaticMarkup} from 'react-dom/server';
import {createElement} from 'react';
import * as icons from '@acme/icons';
​
for (const [name, Icon] of Object.entries(icons)) {
if (!name.endsWith('Icon')) continue;
const html = renderToStaticMarkup(createElement(Icon, {className: 'x'}));
if (!html.includes('class="x"')) throw new Error(`${name} drops className`);
if (!html.includes('currentColor')) throw new Error(`${name} has a fixed color`);
}

Library shapes

--from is any import specifier: a package, a package subpath, or a ./ path inside your package. Use the row that matches the library:

Library shipsCommand
Components in one moduleexperimental icon add Bin --from @acme/glyph-library --definition delete
A subpath per styleexperimental icon add Bin --from @acme/glyph-library/solid --id bin --variant solid (a variant of an icon you added; the set must declare it in variants)
One file per icon, default exportexperimental icon add Rocket --from @acme/glyph-library/rocket --default --definition rocket
Icon data objectsDeclare an adapter, then experimental icon add Rocket --from @acme/glyph-data --definition rocket
SVG strings or URLsDeclare an adapter that renders them, then experimental icon add as above
Your own component filesexperimental icon add Rocket --from ./src/icons/rocket.js --definition rocket

An export that is not a component goes through your adapter: a function in your package that turns it into one. Declare it in the data file, "adapter": {"module": "./src/adapt.js", "export": "adapt"}, and experimental icon add writes export const RocketIcon = adaptIcon(RocketIconSource);:

js
// src/adapt.js
import {createElement} from 'react';
​
/** Turn one icon data object ({path}) into an icon component. */
export function adapt(data) {
return function AdaptedIcon(props) {
return createElement(
'svg',
{'viewBox': '0 0 24 24', 'width': '1em', 'height': '1em', 'fill': 'currentColor', ...props},
createElement('path', {d: data.path}),
);
};
}

The theme map

The generated module also exports themeIcons: one entry per built-in definition in map, each an element of your own icon, hidden from assistive technology and sized 1em. The CLI rewrites it on every experimental icon add and experimental icon map, so you never write it by hand, and integration check proves it matches map. A theme uses it in one line:

tsx
import {defineTheme} from '@astryxdesign/core/theme';
import {themeIcons} from '@acme/icons';
​
export const brand = defineTheme({name: 'brand', icons: themeIcons});

Check the set

npx @astryxdesign/cli integration check reads the set without running your icons. An unmapped built-in definition is an error that names the definition's description and words, so you or your agent can pick the icon; the message suggests an experimental icon map command only when one of your icons has the definition's name or one of its words as its whole id, and never guesses. Its icon-set codes:

  • invalid_icon_set (error): the descriptor, data file, or a definition file is invalid, for example a repeated id, an icon with no words, or a map key that is neither an Astryx definition nor one in your definitions folder. The set is withdrawn: apps do not see it.
  • icon_builtin_definition_missing (error): a built-in definition is not in map.
  • icon_definition_unused (warning): a definition you declared is not in map.
  • icon_definition_shadowed (warning): a definition you declared now exists in Astryx; map to Astryx's and remove yours.
  • icon_module_not_public (error), icon_export_name (warning), icon_not_component (error): the export contract above.
  • icon_export_missing (error): an export, the generated module, or the adapter does not exist.
  • icon_module_invalid (error): the set's module, a file it re-exports, or the generated module does not parse; reported once, instead of a missing export per icon.
  • icon_generated_types_mismatch (error): the generated module's declaration file is missing or does not match it.
  • icon_theme_map_mismatch (error): the theme map disagrees with the built-in entries of map.
  • icon_source_version_drift and icon_license_notice_missing (warnings): the data names another library version than the package depends on, or a set with an upstream ships no license notice.

The other Astryx definitions your set leaves unmapped are not issues: a library need not draw every definition. They are listed under "Unmapped icon definitions (optional)", and in the JSON as the validate result's iconSets: for each set, unmappedDefinitions (each {name, description, words}) and suggestions (each {definition, icon}, for any unmapped definition one of your icons fits). They are never counted and never fail a check.

An export fails only when the reader proves it absent; what it cannot judge it does not fail either. An icon export whose module is an exports target you have not built yet; an icon or adapter export it cannot prove either way (behind a re-export it cannot follow, bound to a default or namespace import, or imported from a package that is not installed); and a theme map or theme-map entry it cannot read are listed under "Not checked", and in the JSON as each set's unchecked (each {icon}, {adapter}, or {entry}, with its reason). Build the package, install the package it imports from, export the icon by name, or write the entry as a binding or element to have it checked. An export your built module lacks is still an error.

Errors fail the check. Warnings fail only with --strict. A set that maps every built-in definition and every definition it declares passes --strict, which integrations run in their build (npx @astryxdesign/cli docs authoring/ship/checks). The flow, start to finish: experimental icon new-set → integration check → experimental icon add (or experimental icon map) for each definition it lists → integration check passes.

Before you publish

  • Run npx @astryxdesign/cli integration verify yourself before publishing. Do not put it in prepublishOnly: under npm publish --dry-run its own npm pack becomes a dry run too, and it checks nothing.
  • Declare @astryxdesign/cli as an optional peer (peerDependenciesMeta). A required peer an app cannot satisfy makes npm remove the app's CLI without a word. Icon sets are experimental, so no version range promises them: any CLI release that has npx @astryxdesign/cli experimental icon reads a set, and the format may change in a patch release. A CLI without it reads experimental as a manifest key it does not know: npx @astryxdesign/cli doctor there warns that your package was published against a newer Astryx CLI, other commands print a one-line nudge to run it, and your icons, icon definitions, and template icon mapping do not appear (npx @astryxdesign/cli docs authoring/ship/versioning).
  • Your package needs react (the theme map and adapter use it): declare it as a peer, as Core does.

Share a definition with every set

Your own definitions need no change to Astryx. To share one with every set, open a pull request that copies its file into the CLI's icon definitions folder, with a description and words. Its name must not clash with another definition.

How apps use it

bash
npx @astryxdesign/cli experimental icon delete # ranked matches with import lines
npx @astryxdesign/cli experimental icon outline/bin # one icon and every variant
npx @astryxdesign/cli experimental icon --list # installed sets and their definition coverage
npx @astryxdesign/cli experimental icon use outline # make it the project's icon set

A template declares, in its descriptor's icons, the definition each imported icon draws ({TrashIcon: 'delete'}). Once the project sets experimental.iconSet in astryx.config (experimental icon use writes it), each declared icon becomes the set's icon for that definition. An icon the set does not map keeps its original import, and the copy reports every icon as mapped or kept. Nothing is guessed from names.

Where icon sets show up

Every place the CLI reads or reports icon sets, all experimental. Outside npx @astryxdesign/cli experimental icon, each appears only in a project that sets experimental.iconSet; npx @astryxdesign/cli search never returns icons:

WhereWhat
experimental.iconSet in astryx.configThe project's icon set, by set name or as <package>/<set>, and the opt-in: templates, discover, and doctor read icon sets only when it is set. npx @astryxdesign/cli experimental icon ranks its icons first, and without it treats the only installed set as the project's. npx @astryxdesign/cli experimental icon use writes it.
experimental.iconSets in the integration manifestThe folder that holds the package's icon sets, relative to package.json. npx @astryxdesign/cli experimental icon new-set declares it.
A template descriptor's iconsThe icon definition each imported icon draws ({TrashIcon: 'delete'}): a named import by the name it imports, a default import by its local name. npx @astryxdesign/cli template, shown or copied, then writes the set's icons and reports icons (status, set, mapped, kept): every key is mapped, or kept with why (no match, a namespace or type-only import, a re-export, an Astryx or project-set import, or no import at all). integration check notes a key the template's source never imports (template_icon_not_imported), never failing.
npx @astryxdesign/cli discoverA package's sets (iconSets) and, for @scope/name/<set>, the set's display name, license, icon count, and definitions; --type icon-set keeps only sets.
npx @astryxdesign/cli doctorThe icon-sets check: fails when experimental.iconSet names a set that is not installed or was withdrawn (an invalid set, or one whose module does not resolve), saying why, and warns when two packages ship a set by that name or a set was withdrawn.
JSON typesicon.search, icon.detail, icon.list, icon.use, icon.map, icon.add, icon.definitionMatches, icon.newSet, and icon.newDefinition, from the matching npx @astryxdesign/cli experimental icon command.