Icons #
The library ships a small, curated set of Phosphor icons as a tree-shakeable subpath export. Library chrome (disclosure carets, close ×, the modified-tab dot, scroll/step arrows) draws from this set, and you can import any of them for your own buttons.
import { Copy, Lock, Trash } from '@dolphinspired/solid-stage/icons';
<IconButton icon={Copy} title="Copy" onClick={copy} />
Each icon is a Component<{ size?; class? }> carrying a stage-icon stage-icon-<name> class, so it sizes off --stage-icon-size and inherits
currentColor. Phosphor glyphs are filled vector shapes (not strokes), so
they stay crisp at small chrome sizes. Unused icons drop via tree-shaking.
The IconComponent contract #
Anywhere the library takes an icon — IconButton, IconToggle,
Section's headerActions, IconButtonField, etc. — the prop type is the
empty-props component shape:
type IconComponent = Component; // = (props: {}) => JSX.Element
i.e. any component renderable with no required props. The library never passes
props to an icon — it's sized and coloured entirely in CSS via the
.stage-icon class (--stage-icon-size / --stage-color-icon). The contract is
deliberately this loose so every common icon shape satisfies it:
- A bundled icon —
import { Copy } from '@dolphinspired/solid-stage/icons'(no extra dependency; already carries.stage-icon). - An Iconify pack via
unplugin-icons— components typedComponent<SvgSVGAttributes>(see the recipe below). - Your own icon component —
lucide-solid, a hand-rolled SVG, anything.
import { Heart } from 'lucide-solid';
<IconButton icon={Heart} title="Favourite" onClick={fav} />
An earlier contract was
Component<{ size? }>, which rejected SVG-attribute icon components (Iconify/unplugin) — their props share no keys with{ size? }. Sizing lives in CSS regardless, so the contract dropped thesizeprop.
"Any icon by name" — Iconify via unplugin-icons #
For the "I just want any Phosphor icon, by name, without hand-maintaining an
adapter" case, wire unplugin-icons
into your app's build. Tree-shaken SVG (only the icons you import ship), any
Iconify pack, themed to match the bundled
set. The library ships a pre-configured preset, stageIcons() (the
@dolphinspired/solid-stage/vite entry), so you don't hardcode the library's
.stage-icon class in your build config:
// vite.config.ts
import { stageIcons } from '@dolphinspired/solid-stage/vite';
plugins: [solid(), stageIcons()], // = Solid compiler + every icon tagged .stage-icon
// tsconfig.json — type the virtual modules
{ "compilerOptions": { "types": ["unplugin-icons/types/solid"] } }
pnpm add -D unplugin-icons @iconify-json/ph # the preset's peer + your pack(s)
// then anywhere — weight is a name suffix: -bold / -fill / -duotone / -thin / -light
import HorseBold from '~icons/ph/horse-bold';
<IconButton icon={HorseBold} title="Horse" onClick={…} /> // matches IconComponent
stageIcons(options) forwards any unplugin-icons option
(yours override the compiler / defaultClass defaults). Prefer not to take the
preset? It's exactly Icons({ compiler: 'solid', defaultClass: 'stage-icon' })
from unplugin-icons/vite — use that directly and skip the peer dependency.
Domain glyphs (a model picker's smileys, media-transport) belong here — see below.
Every icon slot uses this same IconComponent contract — IconButton,
IconToggle, Section's headerActions, IconButtonField, ActionButton's
icon, ButtonGroupOption's icon, SelectFieldAction's icon. Always pass
the component itself (icon={Copy}), not a thunk. (A () => <Copy /> thunk still
type-checks — a zero-arg function satisfies the contract — but the component form
is the convention.)
Bundled set #
The current set (import names are PascalCase):
Chrome glyphs — Info, Plus, X, CaretUp, CaretDown, CaretLeft,
CaretRight, CaretDoubleLeft, CaretDoubleRight, CircleFill,
DotsSixVertical, ArrowsIn, ArrowsInSimple.
Common editor icons — Copy, Lock, Stack, FloppyDisk,
ArrowCounterClockwise, Trash, Eye, EyeSlash, GridNine,
Crosshair, TextT, Hash.
InfoIcon (the field info bubble glyph) is also exported from the main entry.
Styling #
Set the icon size globally via --stage-icon-size (it derives from the size tier:
12 / 16 / 20px at small / normal / large). Target a specific glyph with its class:
.stage-icon-trash { color: var(--stage-color-error); }
Adding to the set (maintainers) #
The set is generated from the @phosphor-icons/core dev dependency into
src/icons/generated/ (committed) by scripts/gen-icons.mjs, driven by the
scripts/icons.json manifest. Add a name to the manifest and regenerate. The
artwork is vendored under Phosphor's MIT licence (retained in NOTICE).
What belongs in the bundled set #
The bundled set is the chrome's own vocabulary — a glyph earns a place only
when a solid-stage component renders it (disclosure carets, the drag grip,
the arrows-in-simple redock action, close ×, the modified-tab dot, scroll/step
arrows). Bundling
those guarantees the chrome looks right with no extra dependency, at a
consistent weight, tree-shaken.
Domain icons are not bundled — media-transport (play / pause / skip),
app-specific marks (a model picker's smileys), or any glyph used only by the
consumer's own UI. There's no principled stopping point once we add icons for
one consumer's feature (record → eject → upload → folder …), and the result
would be a hand-maintained Phosphor subset strictly worse than the consumer
importing lucide-solid / @phosphor-icons/web directly. The
IconComponent contract already covers this:
any Component<{ size? }> works, so consumers bring their own domain glyphs with
zero maintenance cost to the library. If a
glyph later becomes one a library component renders, it earns its place then —
not before.