@cdx-ui/icons
v0.0.1-beta.107
Published
Cross-platform icon library for Forge UI, built on [Material Symbols](https://fonts.google.com/icons) (outlined style, weight 400, optical size 24). Renders via `react-native-svg` and targets iOS, Android, and Web from a single codebase.
Readme
@cdx-ui/icons
Cross-platform icon library for Forge UI, built on Material Symbols (outlined style, weight 400, optical size 24). Renders via react-native-svg and targets iOS, Android, and Web from a single codebase.
~3,800+ Material Symbols available from Google's metadata; ~3,540 are included in the Figma Code Connect batch manifest. Supports outlined/filled variants, plus custom SVGs.
Installation
pnpm add @cdx-ui/iconsPeer dependencies
| Package | Version |
| ------------------ | ----------------------------------- |
| react | ^18.2.0 \|\| ^19.0.0 |
| react-native | >=0.76.0 (optional) |
| react-native-svg | ^13.0.0 \|\| ^14.0.0 \|\| ^15.0.0 |
Usage
import { Home, Favorite, Settings } from '@cdx-ui/icons';
// Outlined (default)
<Home size={24} color="blue" />
// Filled variant
<Favorite size={24} color="red" filled />
// Toggle between variants
<Home filled={isActive} />Each icon is a forwardRef component that accepts all standard SvgProps plus the props below.
Props
| Prop | Type | Default | Description |
| -------------------- | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| size | number \| string | 24 | Sets both width and height. |
| color | string | 'currentColor' | Fill color applied to the icon. |
| filled | boolean | false | Renders the filled variant when true. Falls back to outlined if no filled data exists. |
| accessibilityLabel | string | — | When provided, the icon becomes informative (accessible={true}, accessibilityRole="image"). When omitted, the icon is decorative by default. |
| data-testid | string | — | Forwarded to the root <Svg> element for testing. |
All other props are spread onto the root <Svg> element, including standard React Native accessibility props.
Factory and types
| Export | Description |
| ----------------- | --------------------------------------------------------------------------- |
| createForgeIcon | Factory function to create custom Forge-style icon components from SVG data |
| ForgeIconProps | Props interface for icons created by the factory |
| ForgeIcon | Type alias for a Forge icon component |
| IconNode | Type for SVG path data consumed by the factory |
| IconData | Type for outlined + filled path data pair |
createCdxIconandCdxIconProps/CdxIconare deprecated aliases — use theForgeequivalents.
Naming conventions
Every icon is available under three export patterns:
| Pattern | Example | Use case |
| ---------- | ---------- | ----------------------------------------------- |
| PascalCase | Home | Primary import |
| Suffixed | HomeIcon | Avoids collision with your own Home component |
| Prefixed | CdxHome | Namespace when mixing multiple icon libraries |
import { Home } from '@cdx-ui/icons';
import { HomeIcon } from '@cdx-ui/icons';
import { CdxHome } from '@cdx-ui/icons';Aliases
Icons can have metadata-driven aliases defined in icon-metadata.json. Aliases let you reference the same underlying icon under alternative names — useful for legacy compatibility, domain-specific terminology, or aligning with names used by other icon sets. The generation script produces alias re-exports alongside the standard exports, so aliases are zero-cost at runtime.
Each entry in icon-metadata.json maps a base icon name (matching its SVG filename) to an array of aliases. An alias can be a plain string or an object with deprecated: true and a deprecationReason to flag names that should be migrated away from.
// icon-metadata.json
{
"home": {
"aliases": [
"house",
{
"name": "old-home-icon",
"deprecated": true,
"deprecationReason": "Renamed to Home",
},
],
},
"settings": {
"aliases": ["gear", "preferences"],
},
}The config above produces the following behavior:
House,HouseIcon, andCdxHouseall resolve to theHomeiconGearandPreferencesresolve to theSettingsiconOldHomeIconresolves toHomebut shows a@deprecated Renamed to HomeJSDoc warning in your editor, nudging consumers toward the canonical name
import { House } from '@cdx-ui/icons'; // alias → Home
import { Gear } from '@cdx-ui/icons'; // alias → Settings
import { OldHomeIcon } from '@cdx-ui/icons'; // deprecated alias → Home (editor warning)Aliases that collide with an existing base icon name are silently skipped during generation to avoid duplicate exports.
Accessibility
Icons are decorative by default — hidden from assistive technology when no accessibilityLabel is provided. This is the correct behavior for icons next to text labels or inside labeled containers.
// Decorative (default) — screen readers skip this
<Home size={20} color={theme.colors.icon} />
// Informative — screen readers announce "Home, image"
<Home size={20} accessibilityLabel="Home" />| Scenario | What to do |
| --------------------------------- | ----------------------------------------------------------------- |
| Icon next to a text label | Leave as default (decorative) |
| Icon inside a labeled Button | Leave as default — the button's label covers it |
| Standalone icon conveying meaning | Add accessibilityLabel describing the icon's purpose in context |
| Icon-only button | Label the Pressable wrapper, not the icon |
Cross-platform behavior:
- iOS (VoiceOver): Announced as "{label}, image"
- Android (TalkBack): Announced as "{label}, graphic"
- Web (React Native Web): Renders
role="img"+aria-label
React Native >=0.76 aliases aria-label to accessibilityLabel, so either prop works.
For full details, see 08-accessibility.md.
Custom icons
Place custom SVGs in icons/custom/. The generation script reads from both icons/material/outlined/ and icons/custom/, so custom icons are treated identically to Material Symbols icons.
- Use
{name}.svgfor the outlined variant - Optionally add
{name}-fill.svgfor a filled variant - Author the path in the
0 -960 960 960coordinate space.createForgeIconforcesviewBox="0 -960 960 960"on every icon, and the generator keeps only the root's child elements — it discards each SVG's ownviewBox/width/height. A standard 24px export (path coordinates in0..24) is not compatible: it renders as a tiny speck outside the viewBox (effectively invisible). - Drawable elements must be direct children of
<svg>. The parser is one level deep, so a<path>nested inside a<g>is dropped. Flatten any group (and bake its transform into the pathd) before committing.
To convert a 24px brand/Figma export into the 960 grid, apply the affine transform
x' = 40·x, y' = 40·y − 960 (uniform scale ×40, then translate y by −960) to the path data —
e.g. with svgpath:
svgpath(d).scale(40).translate(0, -960).round(2).toString(). See icons/custom/whats_app.svg
for a normalized reference. Note that icons render monochrome in currentColor; any source
fill is overridden by the color prop.
Brand logos
Brand logos (WhatsApp, Google, GitHub, …) are named Logo<Brand> in the Figma Icon Library,
but are published without the prefix and with brand casing — e.g. Figma LogoWhatsApp →
export WhatsApp. Name the SVG with the de-prefixed, snake_cased filename so the generator
produces the right export:
| Figma component | SVG filename | Export |
| --------------- | --------------- | ---------- |
| LogoWhatsApp | whats_app.svg | WhatsApp |
| LogoYouTube | you_tube.svg | YouTube |
| LogoGitHub | git_hub.svg | GitHub |
| LogoLinkedIn | linked_in.svg | LinkedIn |
The Code Connect batch script (scripts/generate-icon-batch.ts) strips the Logo prefix when
cross-referencing Figma components against local files, so the mapping resolves automatically.
Material Symbols that genuinely start with Logo (e.g. logo_dev) match by exact name first
and are unaffected.
Development
Downloading Material Symbols SVGs
SVGs are fetched directly from the Google Fonts CDN by a custom download script (scripts/download-icons.ts). The script queries Google's metadata API for the active icon list, then downloads only the outlined style (weight 400, opsz 24) with both fill states. Downloaded SVGs are not checked into git.
pnpm --filter @cdx-ui/icons download-iconsThis downloads ~7,600 SVGs (~3,800 icons × 2 fill states) to icons/material/outlined/. Pass --clean to re-download from scratch instead of skipping existing files.
Generating icon components
The generation script (scripts/generate-icons.ts) reads SVGs from both icons/material/outlined/ and icons/custom/, parses them with svgson, and writes TypeScript files to src/icons/ and src/aliases/.
pnpm --filter @cdx-ui/icons generateGenerated files include:
src/icons/{name}.ts— one file per icon with both outlined and filled path datasrc/icons/index.ts— barrel export of all iconssrc/aliases/aliases.ts— metadata-driven legacy name mappingssrc/aliases/suffixed.ts—{Name}Iconexports for every icon and aliassrc/aliases/prefixed.ts—Cdx{Name}exports for every icon and alias
Each generated icon file includes a base64-encoded @preview JSDoc tag, so hovering over an icon import in your editor shows a visual preview.
Generating bottom-tab icons
Native bottom tabs need image assets, not React components. A second generation script (scripts/generate-tab-icons.ts) rasterizes the curated subset declared in tab-icons.config.json into monochrome template PNGs (24/48/72 px → @1x/@2x/@3x) using @resvg/resvg-js, and emits platform-split descriptor modules consumed via the @cdx-ui/icons/tab-icons subpath export.
pnpm --filter @cdx-ui/icons generate:tab-iconsGenerated files (gitignored, like src/icons/):
src/tab-icons/assets/{name}[-fill][@2x|@3x].png— template PNGs (black + alpha; the tab bar tints them at runtime)src/tab-icons/generated/{name}.ts— web descriptor (SVG component only, no asset imports)src/tab-icons/generated/{name}.native.ts— native descriptor with staticrequire()asset importssrc/tab-icons/generated/index.ts— barrel exporting{Name}TabIcondescriptors
To add an icon to the curated set, add its base name to tab-icons.config.json and rebuild — unknown names fail the build. Consumer-facing usage is documented in docs/icons.md § Bottom Tab Icons.
Building
pnpm --filter @cdx-ui/icons buildThis runs clean -> generate -> generate:tab-icons -> bob build. Output goes to lib/ with three targets:
lib/commonjs/— CJS moduleslib/module/— ESM moduleslib/typescript/— per-file.d.tsdeclarations
The package is compiled by react-native-builder-bob, consistent with all other workspace packages. Unlike other workspace packages, the react-native field points to lib/module/index.js (the pre-built ESM output) rather than source. This means the icons package must be built (pnpm build:packages or pnpm --filter @cdx-ui/icons build) before Metro can resolve it. Source maps are excluded from the published package (!lib/**/*.map in files) to reduce install size.
Package structure
icons/
├── icons/
│ ├── material/outlined/ # Downloaded SVGs (not checked in)
│ └── custom/ # Hand-authored custom SVGs
├── scripts/
│ ├── download-icons.ts # Downloads outlined SVGs from Google Fonts CDN
│ ├── generate-icons.ts # SVG → TypeScript generation script
│ ├── generate-tab-icons.ts # SVG → template PNG + descriptor generation
│ └── shared.ts # Naming utilities shared by both generators
├── src/
│ ├── icons/ # Generated icon components
│ ├── aliases/ # Generated alias re-exports
│ ├── tab-icons/ # Bottom-tab descriptors (entry + generated assets)
│ ├── createForgeIcon.ts # Factory function
│ ├── defaultAttributes.ts # SVG defaults (viewBox, fill)
│ ├── types.ts # TypeScript types
│ └── index.ts # Main entry point
├── types/png.d.ts # Ambient *.png module declaration (kept out of src/)
├── icon-metadata.json # Alias and deprecation metadata
├── tab-icons.config.json # Curated bottom-tab icon subset
├── package.json
├── tsconfig.json
└── tsconfig.build.jsonsrc/icons/, src/aliases/, src/tab-icons/assets/, and src/tab-icons/generated/ contain generated files (gitignored except for .gitkeep sentinels).
Architecture
The package follows the patterns established by lucide-react-native, adapted for Material Symbols. Key differences:
- Fill-based icons — Material Symbols icons use
fillexclusively (nostrokeattributes). Thecolorprop maps tofill. - 960x960 coordinate system —
viewBox="0 -960 960 960"instead of Lucide's0 0 24 24. Thesizeprop controls rendered dimensions; the viewBox handles internal coordinates. - Outlined/filled variants — Each icon bundles both variants via the
filledprop, modeling Material Symbols' fill axis. This halves the export count vs. separate components. - Decorative by default — Icons are hidden from assistive technology unless
accessibilityLabelis provided. - OTA compatibility —
childDefaultAttributesexplicitly setsfillon every child SVG element, working around an inheritance bug in CodePush/expo-updates.
For the full research and design rationale, see docs/internal/research/lucide-react-native/.
