Brik Design System
Components

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:

NeedComponentNotes
UI glyphIconph:*, outline-bold by default
Platform / brand mark (google, yelp, linkedin…)SocialIconReal brand marks, brand-colored — never ph:*-logo
Contact mark (phone, email, website…)ContactIconContact marks with no brand identity
Animated stateAnimatedIconLottie wrapper
Per-service glyphServiceTagCustom SVGs via mask-image, not Phosphor
Browse the vocabularyicon catalogReference 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); fill drops the counter for a solid silhouette; duotone sits outside the axis. outline-bold is a heavier outline, still hollow — Phosphor's old bold name hid that next to fill.
  • stroke — outline-thin → outline-light → outline → outline-bold order 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:

weightRendersUse
outline-boldph:name-boldDefault. BDS standard — most contexts.
outlineph:namePhosphor's thin regular outline
outline-lightph:name-lightDecorative or quiet contexts
outline-thinph:name-thinRare; very specific design intent
fillph:name-fillActive/selected states, status badges
duotoneph:name-duotoneEmpty 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:

TokenUse
--bds-text-primaryDefault icon next to body text
--bds-text-secondary / --bds-text-mutedQuiet / secondary icons
--bds-text-linkIcons inside links
--bds-text-positive / --bds-text-negative / --bds-text-infoStatus icons
--bds-text-inverseIcons 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

  1. Find the icon name at phosphoricons.com — copy the kebab-case name.
  2. Add a named constant to components/icons.ts using the ph:icon-name format.
  3. (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-label directly on the surrounding element.

On this page

💬