Icons
The BDS Icon wrapper — Phosphor icons from a bundled offline subset, outline-bold by default.
BDS ships its own Icon component — an offline-first wrapper around @iconify/react that renders Phosphor (ph:*) icons from a bundled subset. Icons render as inline SVGs with no runtime CDN fetch, no flash-of-unstyled-icon, and no bundle bloat from importing the full set. It is API-compatible with @iconify/react's <Icon>, and defaults ph:* icons to outline-bold weight (BDS's standard line density).
Setup
No consumer setup is required — importing Icon from @brikdesigns/bds auto-registers the bundled Phosphor subset (components/icons.generated.json) into Iconify's global store on module load, so ph:* icons resolve synchronously with no network call. A ph:* icon not yet in the subset falls through to Iconify's default behavior (a runtime CDN fetch). To bring your own icon collections offline, call addBrikIcons(collection) once at app start.
Use it for
- Inline icons in buttons, badges, tooltips, fields
- Decorative icons in cards and headers
- Status / state markers paired with text
- Anywhere a small (≤ 32px) glyph carries meaning
Icon is the UI-glyph atom. Brand marks, contact marks, animated states, and service-line glyphs each route to a different component — see the table below.
Choosing an icon component
BDS has five icon-family components. Icon is only one of them; picking the right one is the first decision, before any ph:* name:
| Need | Component | Notes |
|---|---|---|
| UI glyph | Icon | ph:*, outline-bold by default |
| Platform / brand mark (google, yelp, linkedin…) | SocialIcon | Real brand marks, brand-colored — never ph:*-logo |
| Contact mark (phone, email, website…) | ContactIcon | Contact marks with no brand identity |
| Animated state | AnimatedIcon | Lottie wrapper |
| Per-service glyph | ServiceTag | Custom SVGs via mask-image, not Phosphor |
| Browse the vocabulary | icon catalog | Reference page — exports nothing |
Brand and platform marks use SocialIcon, never ph:*-logo. Phosphor ships ph:google-logo, but <SocialIcon platform="google"> is the correct answer — a real brand mark, brand-colored via --bds-color-social-google, across every supported platform. Reaching for ph:*-logo ships the wrong glyph at the wrong color.
Import
import { Icon } from '@brikdesigns/bds';addBrikIcons is also exported from @brikdesigns/bds for registering extra offline collections.
Usage
Direct string reference
The icon prop accepts a collection:name string. Phosphor icons use the ph: prefix.
<Icon icon="ph:star-fill" />
<Icon icon="ph:check" />
<Icon icon="ph:x-circle" />Find available icons at phosphoricons.com.
BDS icon constants
Inside the BDS repo, component authors prefer the named constants in components/icons.ts — they protect against typos and centralize the icon vocabulary. These are not part of the public package export; they're imported by relative path from within components/:
import { Star, Check, XCircle } from '../../icons';
<Icon icon={Star} />Constants follow the pattern export const Name = 'ph:icon-name'. Consumers of @brikdesigns/bds use the ph:* string reference directly (above).
Sizing
Iconify icons inherit font-size from their parent or accept explicit width / height props.
{/* Inherits parent font-size — recommended */}
<span style={{ fontSize: 24 }}>
<Icon icon="ph:star-fill" />
</span>
{/* Explicit size */}
<Icon icon="ph:star-fill" width={24} height={24} />For inline icons inside text, use 1em sizing so the icon scales with the surrounding text:
<button>
<Icon icon="ph:plus" style={{ fontSize: '1em' }} />
Add item
</button>Icon weight
BDS renders ph:* icons at outline-bold weight by default — BDS's standard line density (Phosphor's own regular outline reads as a hairline at small UI sizes). Set the weight prop to change it. An icon whose name already encodes a weight (ph:star-fill) keeps it; the weight prop only rewrites otherwise-unweighted ph:* names, and non-ph:* icons ignore it.
Canonical convention: outline-bold is the BDS UI icon weight on every surface — navigation, form controls, and general glyphs alike. There is no per-surface split; fill is reserved for active/selected states and status. Because the atom applies outline-bold by default, routing a glyph through <Icon> is enough to inherit the canonical weight — only reach for the weight prop to opt a specific icon back to outline (or another weight) for a deliberate reason.
<Icon icon="ph:star" /> {/* outline-bold (default) */}
<Icon icon="ph:star" weight="outline" /> {/* Phosphor's thin regular outline */}
<Icon icon="ph:star-fill" /> {/* solid — explicit weight, left as-is */}The form × stroke axis
Icon weight is a two-part axis, and the names say which part they move — this is why the vocabulary is Brik's, not Phosphor's flat list (ADR-036):
- form —
outline*is hollow (an enclosed glyph keeps its counter-path);filldrops the counter for a solid silhouette;duotonesits outside the axis.outline-boldis a heavier outline, still hollow — Phosphor's oldboldname hid that next tofill. - stroke —
outline-thin→outline-light→outline→outline-boldorder the outline from lightest to heaviest.
You can read the form straight off the SVG path data: ph:chat-circle (regular) and ph:chat-circle-bold both carry 4 subpaths (outer shape + counter → hollow), while ph:chat-circle-fill carries 3 (counter dropped → solid).
weight accepts six semantic names:
weight | Renders | Use |
|---|---|---|
outline-bold | ph:name-bold | Default. BDS standard — most contexts. |
outline | ph:name | Phosphor's thin regular outline |
outline-light | ph:name-light | Decorative or quiet contexts |
outline-thin | ph:name-thin | Rare; very specific design intent |
fill | ph:name-fill | Active/selected states, status badges |
duotone | ph:name-duotone | Empty states, illustrations |
The former Phosphor-flavored names regular (→ outline) and bold (→ outline-bold) still work as @deprecated aliases through one minor, so consumers migrate on their own cadence.
Linear-glyph carve-out. On a glyph with no enclosed area (ph:arrows-clockwise, ph:minus), fill is a visual no-op — the -fill variant is identical geometry. Reach for outline-bold, not fill, on linear glyphs; detect the carve-out with the subpath test, never by eye.
Color and theming
Iconify icons render with fill="currentColor", so they inherit color from the CSS color of any ancestor — no per-icon fill prop needed. To recolor an icon, set color on it or a parent, ideally to a BDS text token rather than a raw hex:
{/* inherits the surrounding text color */}
<span style={{ color: 'var(--bds-text-muted)' }}>
<Icon icon="ph:info" /> Quiet hint
</span>
{/* status icon */}
<Icon icon="ph:check-circle-fill" style={{ color: 'var(--bds-text-positive)' }} />Text tokens commonly used for icon fill:
| Token | Use |
|---|---|
--bds-text-primary | Default icon next to body text |
--bds-text-secondary / --bds-text-muted | Quiet / secondary icons |
--bds-text-link | Icons inside links |
--bds-text-positive / --bds-text-negative / --bds-text-info | Status icons |
--bds-text-inverse | Icons on dark / inverted surfaces |
BDS text tokens are mode-aware, so an icon colored with one retones automatically in brik-dark — prefer them over hardcoded colors.
Service-line glyphs
ServiceTag's per-service glyphs are not Phosphor icons — they're custom SVG assets under components/ui/ServiceTag/icons/. They render via CSS mask-image with background-color: currentColor (not <img src>), so the fill is CSS-recolorable and inherits the tag's service text token:
.bds-service-tag--marketing { color: var(--bds-text-service-marketing-on-light); }
.bds-service-tag__icon { background-color: currentColor; /* masked glyph */ }Each service line carries a mode-aware --text-service-{line}-on-light / -on-dark pair (brand, marketing, information, product, back-office), so the glyph retones per theme. Reach for ServiceTag rather than a raw <img> whenever you need a service-line glyph — Phosphor does not cover every icon need.
Adding icons to BDS
- Find the icon name at phosphoricons.com — copy the kebab-case name.
- Add a named constant to
components/icons.tsusing theph:icon-nameformat. - (Optional) Add the icon to a category in the Storybook stories so other contributors can find it.
// components/icons.ts
export const Heart = 'ph:heart';
export const HeartFill = 'ph:heart-fill';When not to use
Don't use raw SVG inline. Iconify provides 200,000+ icons across multiple collections — you almost never need a one-off SVG. If the design calls for a non-Phosphor icon, evaluate whether a Phosphor alternative is acceptable before committing a custom asset.
- Don't use Icons for brand marks. Logos and brand-specific illustrations are static SVG assets, not icon-system components.
- Don't use Icons without semantic context. An icon-only button needs an
aria-label; an icon next to text doesn't.
Accessibility
- Iconify icons render
<svg aria-hidden="true">by default — appropriate for decorative use next to text. - For icon-only triggers (icon-only buttons), pair with IconButton which adds the required
aria-label. - For meaningful icons that aren't paired with text, set
aria-labeldirectly on the surrounding element.
Related
- IconButton — accessible wrapper for icon-only buttons
- Avatar — for person identity
- ServiceTag — for Brik service-line icons (use
variant="icon") - Storybook icon catalog
- External: phosphoricons.com — find icon names