Icon
Icon display component with automatic caching script from the Iconify API.
Installation
npx oward-ui add iconIntroduction
This icon component based on the Iconify API was designed to meet three main requirements:
- Optimize build and JavaScript bundle size by avoiding heavy runtime icon dependencies (like
@iconify/json) - Provide a simple DX requiring only the icon identifier (e.g.,
lucide:alert-triangle), to get as close as possible to a Tailwind-like experience - Automate fetching of missing icons during development
Features
- Automatic download: Icons are fetched from the Iconify API when detected in your code
- Git Ready: Icons are stored locally and ready to be committed
- Optimized local cache: Icons stored as JSON files with tree-shakeable imports, ready to be committed to Git
- Hot Module Replacement: HMR support via the
icon:watchscript for smooth development - Adaptive sizing: Default size of
1emthat adapts to the typographic context, with support for all CSS units (px, em, rem, etc.) - Multiple format support: Supports most file extensions
js,jsx,ts,tsx,mdx,php,vue,astro,svelte,twig
Comparison
| Feature | @oward/icon | Monicon | unplugin-icons | @iconify/react |
|---|---|---|---|---|
| SSR | ✔︎ | ✔︎ | ✔︎ | ❌ |
| Offline | ✔︎ | ✔︎ | ✔︎ | ❌ |
| Git Ready | ✔︎ | ✔︎ | ❌ | ❌ |
| No import | ✔︎ | ✔︎ | ❌ | ✔︎ |
| Dependencies free | ✔︎ | ❌ | ❌ | ❌ |
| Auto-download | ✔︎ | ❌ | ❌ | ✔︎ |
| Framework agnostic(*) | ✔︎ | ❌ | ✔︎ | ❌ |
| Customizable stroke width | ✔︎ | ❌ | ❌ | ❌ |
* The sync script provided with the component can be used with any framework or without a framework.
Component usage
Basic usage
import { Icon } from "@/ui/icon";
<Icon name="lucide:home" />
<Icon name="mdi:account" className="size-6" />
<Icon name="heroicons:heart" style={{ fontSize: "2rem" }} />Icon sizing
By default, icons have a size of 1em, which allows them to automatically adapt to the surrounding text size. You can customize the size via the size prop or with CSS classes:
// Via the size prop (pixels or CSS units)
<Icon name="lucide:home" size={24} />
<Icon name="lucide:home" size="2rem" />
<Icon name="lucide:home" size="1.5em" />
// Via CSS classes
<Icon name="lucide:home" className="size-6" />
<Icon name="lucide:home" className="text-2xl" />Color with currentColor
Icons use currentColor by default, meaning they inherit the text color of their parent element:
// The icon takes the text color
<p className="text-blue-500">
<Icon name="lucide:info" /> Information message
</p>
// Custom color via className
<Icon name="lucide:heart" className="text-red-500" />Customizing stroke-width
For icons with strokes (like Lucide, Tabler, etc.), you can customize the stroke thickness via the strokeWidth prop or CSS variables. The system uses a fallback cascade:
strokeWidthprop /--icon-sw-override: Instance override (highest priority)--{collection}-sw: Collection-specific override (e.g.,--lucide-sw)--icon-sw: Global override for all icons- Original SVG value as final fallback
// Via the strokeWidth prop (for a specific instance)
<Icon name="lucide:home" strokeWidth={1} />
<Icon name="lucide:home" strokeWidth={3} />/* Global configuration in your CSS */
:root {
--icon-sw: 1.5; /* All icons -> Not recommended */
--lucide-sw: 2; /* Lucide only */
--tabler-sw: 1.5; /* Tabler only */
}This feature only applies to icons that use stroke-width in their SVG (like
Lucide, Tabler, Heroicons outline, etc.). Filled icons are not affected.
Caution with asChild
Usage with asChild
When used with asChild (like in TooltipTrigger, Button, etc.), this can cause SSR hydration errors because cloning the server element can create inconsistencies between server and client rendering.
// ❌ Avoid - causes hydration errors
<TooltipTrigger asChild>
<Icon name="mdi:home" />
</TooltipTrigger>
// ✅ Correct - no hydration error
<TooltipTrigger>
<Icon name="mdi:home" />
</TooltipTrigger>
// ✅ Or wrap in an HTML element
<TooltipTrigger asChild>
<button>
<Icon name="mdi:home" />
</button>
</TooltipTrigger>Component configuration
Path alias
The component needs a path alias to access the cached icons.
Add the path alias in your tsconfig.json or jsconfig.json:
{
"compilerOptions": {
"paths": {
"#icons": ["./_icons"]
}
}
}Important
The #icons alias points to the icon cache folder (the _icons/ folder).
This alias must point to the same path as the one configured in the icon sync
script (default is root: ./_icons). It allows the Icon component to import
icons dynamically.
Sync script
The system includes an iconify-sync script to scan your code and automatically download the icons used.
Configuration in package.json
{
"scripts": {
"icon:scan": "node scripts/iconify-sync --path ./app",
"icon:watch": "node scripts/iconify-sync --path ./app --watch"
}
}Options
| Option | Shortcut | Description | Default |
|---|---|---|---|
--path <dir> | -p | Directory to scan for icon detection | ./ |
--output <dir> | -o | Output directory for the icon cache | ./ |
--custom <dir> | -c | Directory containing custom Iconify JSON files | - |
--watch | -w | Enable watch mode to automatically re-scan on each change | false |
--help | -h | Display help and usage examples | - |
Examples
# Single scan of the current directory
node scripts/iconify-sync
# Scan a specific directory
node scripts/iconify-sync --path ./app
# Watch mode for development
node scripts/iconify-sync --path ./app --watch
# Custom output folder
node scripts/iconify-sync --path ./app --output ./custom-path
# Include custom icons
node scripts/iconify-sync --custom ./brand-iconsCustom cache folder
By default, icons are stored in _icons/. To customize:
// package.json
{
"scripts": {
"icon:scan": "node scripts/iconify-sync --output ./custom-path",
"icon:watch": "node scripts/iconify-sync --watch --output ./custom-path"
}
}Don't forget to update the path alias:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"#icons": ["./custom-path/_icons"]
}
}
}Icon detection
The script uses a dynamic regex built from the Iconify API to automatically detect all icon references in the "collection:icon-name" format.
Supported patterns:
Detection works everywhere you use a string:
// ✅ JSX attributes
<Icon name="lucide:star" />
<MyCustomIcon icon="heroicons:heart" />
// ✅ Variables and constants
const icon = "tabler:settings"
const icons = ["mdi:home", "lucide:star"]
// ✅ Objects and function calls
{ icon: "solar:sun-bold" }
getIcon("mdi:check")Unsupported patterns
// ❌ Dynamic values (variables, template literals)
<Icon name={`mdi:${icon}`} />;
// ❌ Invalid collections (not present in Iconify)
("invalid-collection:icon");Performance
The sync script offers optimal performance even on large projects thanks to parallelized scanning, HTTP/2 multiplexing for downloads, and intelligent caching based on file modification time.
For 1120 files with 6281 icon references (110 unique downloaded), the script completes in 465ms.
Custom icons
Add your own icons (logos, custom assets) in Iconify JSON format. To convert your existing SVG files:
# Install @iconify/tools
pnpm add -D @iconify/tools
# Convert an SVG
node scripts/svg-to-iconify.js input.svg output.json my-brand icon-nameThe script optimizes the SVG and converts colors to currentColor.
Iconify Sync configuration
Add the --custom option pointing to the folder containing your custom icons in Iconify JSON format:
// package.json
{
"scripts": {
"icon:scan": "node scripts/iconify-sync --custom ./brand-icons",
"icon:watch": "node scripts/iconify-sync --watch --custom ./brand-icons"
}
}Use your icons directly in the Icon component with their prefix and name defined during conversion:
<Icon name="brand:logo" className="size-8" />Automatic validation
The system validates all files and silently ignores:
- Malformed or non-Iconify JSON (config.json, package.json, etc.)
- Files without
prefixoricons - Icons without
bodyproperty or with invalid types
Only valid Iconify format icons are loaded.
Usage without React
The icon cache (_icons/) can be used with other frameworks. The iconify-sync script automatically scans PHP files in addition to JavaScript/TypeScript files.
Laravel Blade
Create a Blade component for an experience similar to the React Icon component:
// resources/views/components/icon.blade.php
@props(['name', 'size' => 24, 'class' => '', 'strokeWidth' => null])
@php
[$collection, $iconName] = explode(':', $name);
$path = base_path("_icons/{$collection}/{$iconName}.json");
$icon = file_exists($path) ? json_decode(file_get_contents($path)) : null;
$style = $strokeWidth !== null ? "--icon-sw-override: {$strokeWidth};" : '';
@endphp
@if($icon)
<svg
xmlns="http://www.w3.org/2000/svg"
width="{{ $size }}"
height="{{ $size }}"
viewBox="0 0 {{ $icon->width ?? 24 }} {{ $icon->height ?? 24 }}"
fill="currentColor"
@if($style) style="{{ $style }}" @endif
{{ $attributes->merge(['class' => $class]) }}
>{!! $icon->body !!}</svg>
@endif<x-icon name="lucide:home" />
<x-icon name="lucide:activity" :strokeWidth="1.5" />
<x-icon name="mdi:account" size="32" class="text-blue-500" />Recommendations
- Use the script in development environment: It's recommended to use the script only in development to avoid unnecessary API calls during production build.
- No dynamic icon tags: If the Iconify tag is generated dynamically, e.g.,
`mdi:${iconName}`, it will not be detected by the script. - No asChild with Icon: Avoid using the
Iconcomponent withasChildto prevent SSR hydration errors (see Configuration section) - VSCode extension: For a better IDE experience, use the Iconify IntelliSense extension
- Search for icons: Quickly access icons available through the Iconify API at icones.js.org