oward

Icon

Icon display component with automatic caching script from the Iconify API.

beta

Installation

npx oward-ui add icon

Introduction

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:watch script for smooth development
  • Adaptive sizing: Default size of 1em that 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/iconMoniconunplugin-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:

  1. strokeWidth prop / --icon-sw-override: Instance override (highest priority)
  2. --{collection}-sw: Collection-specific override (e.g., --lucide-sw)
  3. --icon-sw: Global override for all icons
  4. 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 */
}
1
2
3
4
5

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

OptionShortcutDescriptionDefault
--path <dir>-pDirectory to scan for icon detection./
--output <dir>-oOutput directory for the icon cache./
--custom <dir>-cDirectory containing custom Iconify JSON files-
--watch-wEnable watch mode to automatically re-scan on each changefalse
--help-hDisplay 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-icons

Custom 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-name

The 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 prefix or icons
  • Icons without body property 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

  1. Use the script in development environment: It's recommended to use the script only in development to avoid unnecessary API calls during production build.
  2. No dynamic icon tags: If the Iconify tag is generated dynamically, e.g., `mdi:${iconName}`, it will not be detected by the script.
  3. No asChild with Icon: Avoid using the Icon component with asChild to prevent SSR hydration errors (see Configuration section)
  4. VSCode extension: For a better IDE experience, use the Iconify IntelliSense extension
  5. Search for icons: Quickly access icons available through the Iconify API at icones.js.org

On this page