<!-- Source: README.md -->

# @codenhub/icons

Zero-runtime icon system for the web. Write `ic-lucide-heart` in your markup and the icon arrives as a CSS mask — no imports, no components, no icon library in the bundle.

`@codenhub/icons` ships 13 generated icon families totalling over 34,000 icons, each with the license notices its artwork requires, so a project can adopt icons without auditing licenses itself. Two opt-in brand families add logos: `simple-icons`, over 3,000 in one brand color each, such as GitHub and npm, and `logos`, over 2,000 in full color, such as Google and Slack. Brand marks stay subject to their owners' trademark guidelines.

## Installation

```sh
pnpm add @codenhub/icons
npm install @codenhub/icons
yarn add @codenhub/icons
bun add @codenhub/icons
```

## Usage

Icons reach a project through CSS. Pick the entry point that matches your pipeline; all three understand the same classes.

### Tailwind CSS v4

One line. Every family except the brand families `simple-icons` and `logos` is resolvable, and Tailwind emits only the icons your markup used. To use a brand family, declare the plugin yourself with `families`, as in [Tailwind](docs/delivery/tailwind.md).

```css
@import "tailwindcss";
@import "@codenhub/icons/tw";
```

### Plain CSS, no build step

Import the base rules, then a stylesheet per family. Each family is complete, so nothing has to scan your markup.

```css
@import "@codenhub/icons";
@import "@codenhub/icons/lucide";
```

### Vite or PostCSS

The plugins scan your markup and generate only the rules it needs, which keeps the output small without Tailwind.

```ts
// vite.config.ts
import lucide from "@codenhub/icons/data/lucide";
import viteIcons from "@codenhub/icons/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [viteIcons({ content: ["./src/**/*.{html,ts,tsx}"], defaultPrefix: "lucide", families: [lucide] })],
});
```

```css
@import "@codenhub/icons";
```

### The markup

An icon is an element carrying an icon class — an `<i>`, or `.ic` on any element you own.

```html
<!-- standalone -->
<i class="ic-lucide-heart" aria-hidden="true"></i>

<!-- icon beside a label: a real element inside the control -->
<button class="btn"><i class="ic-lucide-check" aria-hidden="true"></i>Submit</button>

<!-- .ic, for a component that renders its own tag (CSS-based methods) -->
<span class="ic ic-lucide-search" aria-hidden="true"></span>

<!-- stroke width, for stroke-based families -->
<i class="ic-lucide-heart/1.5" aria-hidden="true"></i>

<!-- a size from the axis -->
<i class="ic-lucide-search ic-lg" aria-hidden="true"></i>
```

Size and color follow custom properties: set `--ic-size` and `--ic-color` on the icon or any ancestor. The `ic-xs`–`ic-xl` classes are shorthand that set `--ic-size` to a rem value; the default is `1em`. Icons paint in the text color, except brand icons, which arrive in their brand color until `--ic-color` says otherwise: `:root { --ic-color: currentColor; }` makes every icon monochrome. There is no `::before`, `::after`, or form-control form — for a `background-image` icon, resolve the `url()` with `getIconMaskUrl` / `getIconCssProps`.

## Documentation

- [Documentation overview](docs/index.md): what the package is, installation, and the shortest working setup.
- [Concepts](docs/concepts.md): families, finding icon names, name resolution, color, stroke width, size, and accessibility.
- [Delivery methods](docs/delivery/index.md): plain CSS, Tailwind CSS v4, the Vite plugin, and the PostCSS plugin.
- [Frameworks](docs/frameworks/index.md): setup for Astro, Next.js, and SvelteKit.
- [JavaScript API](docs/javascript-api.md): the registry, renderers, and third-party set adapter.
- [Licensing](docs/licensing.md): attribution modes and what each build owes.
- [Troubleshooting](docs/troubleshooting.md): an icon that renders as nothing, as a solid square, or at the wrong width.
- [API reference](docs/reference/index.md): every export of the root, `/vite`, and `/postcss` entry points, generated from the source.

## Requirements

| Requirement  | Details                                                                         |
| ------------ | ------------------------------------------------------------------------------- |
| Node.js      | 24 or newer, for the build-time plugins and helpers.                            |
| CSS imports  | Consumer tooling must resolve package CSS imports.                              |
| Browsers     | Must support CSS `mask-image`, which every method uses except inline SVG mode.  |
| Tailwind CSS | Version 4 or newer, and only for the `/tw` and `/tailwind` entry points.        |
| Optional     | `vite` >= 5.0.0 for the Vite plugin, `postcss` >= 8.0.0 for the PostCSS plugin. |

## Notes

- An icon is named `prefix:name`, written `ic-prefix-name` in a class, as in `ic-material-symbols-rounded-home`.
- The package names no default family. An unqualified `ic-heart` resolves only where you said what it means: through `defaultPrefix` in the Vite and PostCSS plugins, through `default:` in the Tailwind plugin, or, in the plugin-free path, through the last family stylesheet you imported that defines it. A family that has no icon by that name does not claim the bare selector, so an earlier family still answers to it, and the brand family stylesheets claim no bare names at all.
- Stroke width is a modifier on the icon class, `ic-lucide-heart/1.5`, and applies only to families drawn with strokes. It needs one of the plugins: a plugin-free stylesheet has no way to know which widths you want, so families there render at their authored width.
- An icon is an `<i>` (or `.ic` on any element, in the CSS-based methods). There is no `::before`, `::after`, or form-control form: an icon beside a label is a real `<i>` in the control, and a `background-image` icon comes from `getIconMaskUrl` / `getIconCssProps`.
- `xs`, `sm`, `md`, `lg`, and `xl` are reserved (`ic-xs`–`ic-xl` are the size axis): no family may use one as a prefix or ship an icon by that name.
- Size follows `--ic-size`, default `1em`. The `ic-xs`–`ic-xl` classes each set it to a rem value (`0.75`, `0.875`, `1`, `1.25`, `1.5`) and nothing else, so they compose with a stroke modifier. An inlined `<svg>` from the renderer or `mode: "svg"` carries a `1em` box of its own and honours `--ic-size` through the base rules.
- The `/lucide` and other family stylesheets are large by construction — a whole family, because nothing is narrowing it. `dist/css` totals about 36 MB across all 15. Import the families you use, or use Tailwind or a plugin, which emit only what your markup asked for.
- `mode: "svg"` in the Vite plugin rewrites `<i class="ic-...">` tags into inline SVG and nothing else. A class on another element gets no icon in this mode; the plugin warns when it finds one.
- Icon names are the upstream family's own: Material Symbols names keep their underscores, as in `ic-material-symbols-rounded-check_circle`.

## License

This package is licensed under the [Apache-2.0](LICENSE) license.

Its bundled, generated icon artwork comes from third parties, under ISC, MIT, Apache-2.0, and CC0 terms. See [NOTICE](NOTICE) for the full list. Each family stylesheet opens with its own notice, and the plugins emit the notices of the families a build actually used, so a consumer carries no separate obligation for them; see the documentation for the `attribution` options that control it. The brand families `simple-icons` and `logos` owe no license notice, but their marks are trademarks of their owners: using one is subject to that owner's brand guidelines, which no build setting satisfies. A third-party set adopted through `adoptIconifySet` is not covered — a consumer remains responsible for its license terms and attribution material.

<!-- Source: docs/index.md -->

# Icons through CSS, with no runtime

`@codenhub/icons` turns an icon class such as `ic-lucide-heart` into a CSS mask rule. There is no component layer, no icon library in your bundle, and nothing to import per icon. The package ships 13 generated icon families totalling over 34,000 icons, each carrying the license notices its artwork requires, plus two opt-in brand families: over 3,000 one-color logos and over 2,000 full-color ones.

## Setup

### Installation

```sh
pnpm add @codenhub/icons
```

### Quick start

Icons reach a project through CSS. The shortest path with no build step is the base rules plus one family stylesheet:

```css
@import "@codenhub/icons";
@import "@codenhub/icons/lucide";
```

```html
<!-- standalone -->
<i class="ic-lucide-heart" aria-hidden="true"></i>

<!-- beside a label -->
<button><i class="ic-lucide-check" aria-hidden="true"></i>Submit</button>
```

An icon is an `<i>` carrying an icon class (or `.ic` on any element, in the CSS-based methods). Size and color follow custom properties: set `--ic-size` and `--ic-color` on the icon or any ancestor, or use the `ic-xs`–`ic-xl` size classes. Icons follow the text color, except brand icons, which arrive in their brand color; see [Color](docs/concepts.md#color).

Four delivery methods understand the same classes — plain CSS, Tailwind CSS v4, a Vite plugin, and a PostCSS plugin. The plugins scan your markup and emit only the rules you used; the plain-CSS family stylesheets are complete and large by construction. See [Delivery methods](docs/delivery/index.md) to choose one, and [Frameworks](docs/frameworks/index.md) for framework-specific setup.

## Requirements

| Requirement  | Details                                                                         |
| ------------ | ------------------------------------------------------------------------------- |
| Node.js      | 24 or newer, for the build-time plugins and helpers.                            |
| CSS imports  | Consumer tooling must resolve package CSS imports.                              |
| Browsers     | Must support CSS `mask-image`, which every method uses except inline SVG mode.  |
| Tailwind CSS | Version 4 or newer, and only for the `/tw` and `/tailwind` entry points.        |
| Optional     | `vite` >= 5.0.0 for the Vite plugin, `postcss` >= 8.0.0 for the PostCSS plugin. |

## Next steps

- [Concepts](docs/concepts.md): families, finding icon names, name resolution, color, stroke width, size, and accessibility.
- [Delivery methods](docs/delivery/index.md): the four ways icons reach your CSS and how to choose.
- [Frameworks](docs/frameworks/index.md): setup for Astro, Next.js, and SvelteKit.
- [JavaScript API](docs/javascript-api.md): the registry, renderers, and third-party set adapter.
- [Licensing](docs/licensing.md): attribution modes and what each build owes.
- [Troubleshooting](docs/troubleshooting.md): an icon that renders as nothing, as a solid square, or at the wrong width.
- [API reference](docs/reference/index.md): every export of `@codenhub/icons`, `/vite`, and `/postcss`.

<!-- Source: docs/concepts.md -->

# How the icon system fits together

Every delivery method reads the same classes and shares the same model. This page describes that model once, independent of which method you use.

## Families

An icon family is a namespace of icons with its own prefix, geometry, and license, such as `lucide` or `material-symbols-rounded-fill`. Families are data, generated from their upstream projects and shipped as modules:

```ts
import lucide from "@codenhub/icons/data/lucide";
import phosphorFill from "@codenhub/icons/data/phosphor-fill";
```

Available families:

| Prefix                                                           | Icons     | Style                       | License    |
| ---------------------------------------------------------------- | --------- | --------------------------- | ---------- |
| `lucide`                                                         | 2159      | outlined, stroke-based      | ISC        |
| `phosphor`                                                       | 1512      | outlined                    | MIT        |
| `phosphor-thin`, `phosphor-light`, `phosphor-bold`               | 1512 each | outlined, other weights     | MIT        |
| `phosphor-fill`                                                  | 1512      | filled                      | MIT        |
| `phosphor-duotone`                                               | 1512      | duotone                     | MIT        |
| `material-symbols-outlined`, `-rounded`, `-sharp`                | 3929 each | outlined                    | Apache-2.0 |
| `material-symbols-outlined-fill`, `-rounded-fill`, `-sharp-fill` | 3929 each | filled                      | Apache-2.0 |
| `simple-icons` (brand)                                           | 3254      | filled logos, brand-colored | CC0-1.0    |
| `logos` (brand)                                                  | 2016      | full-color logos            | CC0-1.0    |

Variants such as filled or duotone are separate families rather than options on one, so resolution stays a prefix and a name.

`simple-icons` and `logos` are brand families. `simple-icons` draws each logo in one brand color, as in `ic-simple-icons-github` and `ic-simple-icons-npm`. `logos` draws them in full color, as in `ic-logos-google-icon` and `ic-logos-slack-icon`, for brands that one color cannot show, such as Google's four-color G. The two complement each other: most brands are in only one of them.

A full-color logo weighs more than a one-color icon. With the plugins, a project pays only for the logos it uses: about 2 KB of CSS for a typical one, and under 8 KB for 95% of them. A few traced, highly detailed logos are much heavier, from about 60 KB to 150 KB each: `lerna` (149 KB), `effector` (120 KB), `composer` (88 KB), `hugo` (85 KB), `tastejs` (69 KB), `matplotlib` (66 KB), `matplotlib-icon` (65 KB), and `chalk` (60 KB). A gzipped or Brotli-compressed stylesheet transfers several times smaller than these figures. Where weight matters, prefer a lighter mark, such as a brand's `-icon` variant, or the brand's `simple-icons` icon when it has one. Brand families are opt-in everywhere — `@codenhub/icons/tw` never resolves them, and a declared `@plugin` resolves them only when its `families` option names them — and their marks are trademarks of their owners. See [Licensing](docs/licensing.md#brand-families).

## Names

An icon is identified by `prefix:name`. A class writes that with a dash, because a class name cannot contain a colon:

```html
<!-- lucide:heart -->
<i class="ic-lucide-heart"></i>

<!-- material-symbols-rounded:home -->
<i class="ic-material-symbols-rounded-home"></i>
```

The longest matching family prefix wins, so `material-symbols-outlined` is preferred over `material` when both are loaded.

An unqualified name resolves against a default family, and against nothing at all when none is configured. The package names no default of its own and ships no curated map of semantic names: `ic-close` means whatever the family you chose calls `close`, and gets no icon if that family has no such icon. In plain CSS an earlier-imported family that does have the name still answers to it, because a family stylesheet claims only the bare names it defines. Lucide's is named `x`, so under Lucide you write `ic-x`.

Where the default comes from depends on the delivery method:

| Method        | Default family                                                                                                                                |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Vite, PostCSS | The `defaultPrefix` option.                                                                                                                   |
| Tailwind      | The `default:` plugin option.                                                                                                                 |
| Plain CSS     | The last core family stylesheet imported. A brand family stylesheet claims no bare names, so importing one last leaves the default as it was. |

```ts
import { IconRegistry } from "@codenhub/icons";
import lucide from "@codenhub/icons/data/lucide";

const registry = new IconRegistry({ defaultPrefix: "lucide" });
registry.registerFamily(lucide);

registry.resolve("heart"); // lucide:heart
registry.resolve("close"); // undefined -- lucide calls it "x"
```

### Finding an icon's name

An icon's name is the one its upstream project gives it, so browse the family's own site and write the name you find there:

| Family                 | Browse at                                                | Example class                                   |
| ---------------------- | -------------------------------------------------------- | ----------------------------------------------- |
| `lucide`               | [lucide.dev/icons](https://lucide.dev/icons)             | `ic-lucide-circle-check`                        |
| `phosphor`, variants   | [phosphoricons.com](https://phosphoricons.com)           | `ic-phosphor-fill-check-circle`                 |
| `material-symbols-*`   | [fonts.google.com/icons](https://fonts.google.com/icons) | `ic-material-symbols-rounded-check_circle`      |
| `simple-icons` (brand) | [simpleicons.org](https://simpleicons.org)               | `ic-simple-icons-github`, `ic-simple-icons-npm` |
| `logos` (brand)        | [svgporn.com](https://svgporn.com)                       | `ic-logos-google`, `ic-logos-google-icon`       |

Material Symbols names keep Google's underscores: `check_circle`, not `check-circle`. A Simple Icons name is the icon's slug, with any `_` written as `-`. A `logos` name is the upstream file name, lowercased, with any `_` or `.` written as `-`: `socket.io` is `ic-logos-socket-io`. Some brands come as a full logo, often a wordmark, and as a symbol with `-icon` appended; some add `-dark` or `-alt` variants. A misspelled name is not an error: the class resolves to nothing, and the element renders as a solid square with the base rules loaded, or as nothing without them, so check the name against the family when an icon does not appear. Each family module also lists its names, as the keys of its `icons` object.

## Reserved names

`ic-xs` through `ic-xl` are the [size axis](#size), so `xs`, `sm`, `md`, `lg`, and `xl` cannot be family prefixes or icon names. Generation refuses a family that claims one. Stroke width is written on the icon class itself, so it claims no prefix.

## Markup

An icon is an element that carries an icon class. Use an `<i>`:

```html
<i class="ic-lucide-search" aria-hidden="true"></i>

<!-- stroke width, for stroke-based families only -->
<i class="ic-lucide-heart/1.5"></i>

<!-- a size from the axis -->
<i class="ic-lucide-search ic-lg"></i>
```

`<i>` is the recommended element and the only one [inline SVG mode](docs/delivery/vite.md#inline-svg-mode) rewrites. In the CSS-based methods the class also works on any element you add `.ic` to, for a framework component that renders its own tag:

```html
<span class="ic ic-lucide-search" aria-hidden="true"></span>
```

There is no `::before`, `::after`, or form-control form: an icon next to a label is a real `<i>` inside the button or link, which composes with a layout gap and works in every delivery method. For an icon painted as a `background-image` — a custom control, an input affix — resolve the `url()` with [`getIconMaskUrl` or `getIconCssProps`](docs/javascript-api.md) and place it yourself.

Size and color follow CSS custom properties:

```css
.toolbar {
  --ic-size: 1.25rem;
  --ic-color: var(--color-accent);
}
```

See [Size](#size) and [Color](#color).

## Color

An icon paints in the first of these that applies:

1. `--ic-color`, set on the icon itself or on any ancestor.
2. The icon's own artwork, for a full-color `logos` icon: `ic-logos-google-icon` keeps all four of Google's colors and its gradients.
3. The icon's own color. Only `simple-icons` icons have one: `ic-simple-icons-github` is GitHub's `#181717`, `ic-simple-icons-npm` is npm's `#cb3837`.
4. `currentColor`, the text color around it.

So every icon follows the text color by default, except brand icons, which arrive in their brand colors. A full-color logo under `--ic-color` becomes a one-color silhouette of its outline: detail drawn in a contrasting color inside the logo, such as the `31` on Google Calendar's page, fills in. Setting `--ic-color` changes that, at whatever scope you set it:

```css
/* every icon on the page, brand icons included, in the text color */
:root {
  --ic-color: currentColor;
}

/* every icon in the toolbar */
.toolbar {
  --ic-color: var(--color-accent);
}

/* brand icons only, back to monochrome */
[class*="ic-simple-icons-"],
[class*="ic-logos-"] {
  --ic-color: currentColor;
}
```

```html
<!-- one icon -->
<i class="ic-simple-icons-npm" style="--ic-color: white" aria-hidden="true"></i>
```

To keep brand colors inside a scope that sets `--ic-color`, reset it on them with `--ic-color: initial`, which lets the brand color through again.

Some brand colors are near-black — GitHub, X, and Vercel among them — and disappear on a dark background. Give those a light color in your dark theme:

```css
@media (prefers-color-scheme: dark) {
  .ic-simple-icons-github,
  .ic-simple-icons-x {
    --ic-color: white;
  }
}
```

With a custom class `prefix`, every property here is renamed to match: `--ux-color` under `prefix: "ux"`. `color` alone does not recolor a brand icon, because its own color comes before `currentColor`; set `--ic-color` instead. All of this works the same in every delivery method and in [inline SVG mode](docs/delivery/vite.md#inline-svg-mode), where the brand colors are carried by the `<svg>` itself and need no stylesheet. One exception there: under a Content Security Policy that blocks `style` attributes, an inlined brand icon keeps its brand colors but `--ic-color` no longer reaches it.

## Accessibility

An icon element has no text content, so assistive technology announces nothing for it, or an unhelpful guess. Say which kind of icon it is:

```html
<!-- decorative, beside a visible label: hide it -->
<button><i class="ic-lucide-trash" aria-hidden="true"></i>Delete</button>

<!-- the only content of a control: label the control, hide the icon -->
<button aria-label="Delete"><i class="ic-lucide-trash" aria-hidden="true"></i></button>

<!-- meaningful on its own, outside a control: give it a role and a name -->
<i class="ic-lucide-triangle-alert" role="img" aria-label="Warning"></i>
```

In [inline SVG mode](docs/delivery/vite.md#inline-svg-mode) the attributes you put on the `<i>` carry over to the `<svg>` that replaces it.

## Stroke width

Stroke width is a modifier on the icon class, and only families drawn with strokes answer to it. Of the bundled families, Lucide is the only one; the rest are drawn as filled paths and ignore it.

```html
<!-- a thinner stroke -->
<i class="ic-lucide-heart/1.5"></i>

<!-- a heavier stroke -->
<i class="ic-lucide-heart/3"></i>
```

The width is baked into the artwork the rule carries, so it cannot be a second class applied on top: `ic-heart` and `ic-heart/1.5` are two icons, not one icon and a switch. One class is one rule, so a project pays for the widths it wrote and no others.

That is also why the modifier needs a build step. It works under Vite, PostCSS, and Tailwind, all of which see your markup; the plugin-free family stylesheets render at the family's authored width, because nothing there can know which widths to prepare.

## Size

The default size is `1em`. Set `--ic-size` on the icon or any ancestor to change it, or add a class from the axis:

| Class   | `--ic-size` |
| ------- | ----------- |
| `ic-xs` | `0.75rem`   |
| `ic-sm` | `0.875rem`  |
| `ic-md` | `1rem`      |
| `ic-lg` | `1.25rem`   |
| `ic-xl` | `1.5rem`    |

```html
<i class="ic-lucide-search ic-lg"></i>
```

Each class does one thing — set `--ic-size` to a rem value — so it composes with a stroke modifier and needs no knowledge of the element it lands on. Unlike stroke width, a size is not baked into the artwork, so the axis is part of the base rules and works in every delivery method: the plugin-free family stylesheets, and [inline SVG mode](docs/delivery/vite.md#inline-svg-mode), where the base rules size the rewritten `<svg>` as long as `@import "@codenhub/icons"` is present. The `.ic` form itself is CSS-based methods only, as above.

The five classes are always emitted, so `xs`, `sm`, `md`, `lg`, and `xl` are [reserved](#reserved-names): a family may not ship an icon or take a prefix by one of those names.

<!-- Source: docs/delivery/index.md -->

# How icons reach your CSS

`@codenhub/icons` produces CSS mask rules. What differs between the delivery methods is only where those per-icon rules come from: a complete stylesheet you import, or a plugin that scans your markup and emits just what you used.

## Entry points

| Entry                      | For                         | What it delivers                                                                        |
| -------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
| `@codenhub/icons`          | Every method                | The base `.ic` rules, about 2 KB. Never rewritten by a plugin.                          |
| `@codenhub/icons/tw`       | Tailwind CSS v4             | Base rules plus the plugin. Every non-brand family resolvable, only used icons emitted. |
| `@codenhub/icons/tailwind` | Tailwind CSS v4, configured | The plugin on its own, so `@plugin` can carry options.                                  |
| `@codenhub/icons/<family>` | Plain CSS, no build step    | One complete family, every icon written out.                                            |
| `@codenhub/icons/vite`     | Vite                        | Scans your markup and generates only the rules it needs.                                |
| `@codenhub/icons/postcss`  | PostCSS                     | The same, for a PostCSS pipeline.                                                       |

`@import "@codenhub/icons"` means exactly one thing everywhere: the base rules the icon classes build on. No plugin rewrites it, no plugin deletes it. What differs between the methods is only where the per-icon mask rules come from.

## Choosing one

| If your project…                                           | Use                          |
| ---------------------------------------------------------- | ---------------------------- |
| Already uses Tailwind CSS v4                               | [Tailwind](docs/delivery/tailwind.md)      |
| Builds with Vite (or a Vite-based framework)               | [Vite plugin](docs/delivery/vite.md)       |
| Runs a PostCSS pipeline but not Vite (Next.js, others)     | [PostCSS plugin](docs/delivery/postcss.md) |
| Has no build step, or you want the simplest possible setup | [Plain CSS](docs/delivery/plain-css.md)    |

The plugin methods keep output small without any manual curation: a project using three icons ships about 10 KB of CSS. The plain-CSS family stylesheets are complete by construction — Lucide is about 1.3 MB, all 15 families about 36 MB — so that method trades size for having no build integration at all.

The `ic-lucide-heart/1.5` stroke-width modifier needs one of the plugin methods; plain CSS renders stroke families at their authored width. See [Concepts](docs/concepts.md) for the shared model and [Frameworks](docs/frameworks/index.md) for framework-specific setup.

<!-- Source: docs/delivery/plain-css.md -->

# Plain CSS, no build step

Import the base rules, then a stylesheet per family. Each family stylesheet is complete, so nothing has to scan your markup.

```css
@import "@codenhub/icons";
@import "@codenhub/icons/phosphor-fill";
@import "@codenhub/icons/lucide";
```

Every rule carries two selectors, the qualified `ic-lucide-heart` and the bare `ic-heart`, sharing one copy of the artwork. That is how this method gets a default family without any configuration to hold one: the last family you import wins every bare name it defines. Above, `ic-heart` is Lucide's.

The brand families are the exception. `@codenhub/icons/simple-icons` and `@codenhub/icons/logos` carry only qualified names, such as `ic-logos-google`, so importing one never changes what `ic-x` or `ic-play` draws.

## Cost

The family stylesheets are large by construction — every icon, because nothing is narrowing them. Lucide is about 1.3 MB, Material Symbols Rounded about 3.9 MB, the full-color `logos` about 6.5 MB, all 15 families about 36 MB. A project chooses its cost by choosing which families it imports. If that is too much, use a plugin method or Tailwind, which emit only what your markup asked for.

## Limitations

Stroke width does not work here. It is baked into the artwork each rule carries, and a stylesheet with no build step behind it cannot know which widths you want. Stroke families render at their authored width.

## See also

- [Delivery methods](docs/delivery/index.md) — how the methods compare.
- [Concepts](docs/concepts.md) — name resolution and the markup forms.

<!-- Source: docs/delivery/tailwind.md -->

# Tailwind CSS v4

```css
@import "tailwindcss";
@import "@codenhub/icons/tw";
```

Every family the package ships is resolvable except the brand families `simple-icons` and `logos`, which only a declared `@plugin` can resolve, by naming them in `families` (below), and Tailwind emits only the icons your markup used. Loading the families costs about 150–190 ms and 112 MB once per build, both brand families included, against about 35 ms and 53 MB for a single family; a project using three icons ends up with about 10 KB of CSS.

## Configuring the plugin

To pass options, declare the plugin yourself instead of importing `/tw`, which already declares it with no options:

```css
@import "tailwindcss";
@import "@codenhub/icons";
@plugin "@codenhub/icons/tailwind" {
  families: lucide, phosphor-fill;
  default: lucide;
  stroke-width: 1.5;
}
```

| Option         | Purpose                                                               |
| -------------- | --------------------------------------------------------------------- |
| `families`     | Family prefixes to resolve against. Defaults to all but brand ones.   |
| `default`      | Family that unqualified names resolve against. There is no default.   |
| `prefix`       | Class prefix. Defaults to `ic`.                                       |
| `stroke-width` | Applied to stroke-based families that carry no modifier of their own. |
| `attribution`  | `auto` or `off`. See [Licensing](docs/licensing.md).                    |

`families` does not make the build cheaper. A Tailwind plugin handler has to be synchronous — utilities registered after an `await` are dropped — so every family is already loaded by the time the options are read. It decides which names resolve, and which notices the output can carry.

## Attribution

`@import "@codenhub/icons/tw"` inlines a static file, so it carries an ordinary `/*! … */` banner naming every family it resolves — a superset notice, which is always safe. Declaring the plugin yourself with `@plugin` narrows resolution, and there the notice is a `--ic-attribution-<family>` custom property on `:root` instead, one per family the build actually used. See [Licensing](docs/licensing.md).

## See also

- [Delivery methods](docs/delivery/index.md) — how the methods compare.
- [Concepts](docs/concepts.md) — name resolution and stroke width.

<!-- Source: docs/delivery/vite.md -->

# Vite plugin

```ts
// vite.config.ts
import lucide from "@codenhub/icons/data/lucide";
import viteIcons from "@codenhub/icons/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [viteIcons({ content: ["./src/**/*.{html,ts,tsx}"], defaultPrefix: "lucide", families: [lucide] })],
});
```

```css
@import "@codenhub/icons";
```

| Option          | Purpose                                                                                     |
| --------------- | ------------------------------------------------------------------------------------------- |
| `families`      | Families to resolve against. Nothing is bundled by default.                                 |
| `defaultPrefix` | Family an unqualified name resolves against. There is no default.                           |
| `content`       | Files and globs to scan, beyond what the build transforms.                                  |
| `prefix`        | Class prefix. Defaults to `ic`.                                                             |
| `strokeWidth`   | Default for stroke-based families whose classes carry no modifier.                          |
| `mode`          | `css` serves mask rules; `svg` replaces `<i class="ic-...">` with inline SVG at build time. |
| `attribution`   | `auto`, `file`, or `off`. See [Licensing](docs/licensing.md).                                 |
| `registry`      | A prepared `IconRegistry`, replacing `families`.                                            |

The generated stylesheet is served as `virtual:icons.css` and injected into every HTML entry point. `@import "@codenhub/icons";` is left alone — it resolves through the package's `style` export condition to the base `.ic` rules and means the same thing whether or not a plugin is in the pipeline. Keep it in your stylesheet; the plugin adds the per-icon rules around it rather than in place of it.

## Scanning

`content` takes literal paths and glob patterns, `src/**/*.{html,tsx}` among them, expanded when the scan runs. It is additive: the plugin also scans every source file the build transforms, so `content` is for what the bundler never reaches, and for making the first paint correct in dev.

The transform-time scan covers `.html`, `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`, `.svelte`, and stylesheet files. Markup in a file type outside that set — a `.astro` or `.md` template — is only seen through `content`, so list those globs explicitly. See [Frameworks](docs/frameworks/index.md). This is about the stylesheet only; which files [inline SVG mode](#inline-svg-mode) rewrites is a separate set.

In dev, Vite serves the HTML before it transforms a single module, so a class the plugin has only ever seen inside a module is not yet known when the page is first served. The stylesheet is therefore served as a module rather than inlined, and the plugin invalidates it whenever a transform turns up something new, so the icon arrives without a reload. Listing your sources in `content` avoids the round trip entirely, because those files are read from disk up front.

In `css` mode, a build with HTML entries writes the stylesheet as a CSS asset named after its content hash, `assets/codenhub-icons-[hash].css` by default, and links it from every one of them. A build transforms modules before the HTML, so the file already holds every class the build found. A build without HTML entries, such as a library build, emits no stylesheet, and neither does [inline SVG mode](#inline-svg-mode). A linked file needs no nonce or hash in a Content Security Policy: `style-src 'self'` allows it when it is served from the page's origin. With a `base` on another origin, such as a CDN, the policy must allow that origin, as it already must for Vite's own CSS. Two alternatives were rejected:

- **An inline `<style>`**, which builds wrote until 0.2.1. A policy without `'unsafe-inline'`, a nonce, or a hash refuses it, so every icon rendered blank in production while dev, which uses a same-origin module script, still worked.
- **A `nonce` option on the plugin.** Vite's own [`html.cspNonce`](https://vite.dev/config/shared-options#html-cspnonce) already stamps a nonce on the tags in the built HTML, and a nonce needs a server that sends a fresh one per response, which a static host cannot do.

## Inline SVG mode

`mode: "svg"` replaces `<i class="ic-...">` tags with inline SVG at build time and emits no stylesheet at all. It rewrites those tags and nothing else, so a class left on any other element — or `.ic` on a `<span>` — renders nothing in this mode: there is no stylesheet to carry its mask. The plugin warns at build time when it finds an icon class on an element it will not rewrite, naming the class and the file.

Switching an existing project from CSS mode is not only a config change, because the element that reaches the browser is no longer the one you wrote. Two things the `<i>` had need attention:

- **Size.** The rendered `<svg>` carries a `1em` box as a presentation attribute, so an icon renders at text size with no stylesheet at all. CSS still wins over that: keep `@import "@codenhub/icons"` in a stylesheet and the base rules size the inlined `<svg>` from `--ic-size` exactly as they size an `<i>`, so setting `--ic-size` on an ancestor and the `ic-xs`–`ic-xl` classes both work. Without that import an icon is fixed at `1em`.
- **`hidden`.** `[hidden] { display: none }` is a user-agent rule for HTML elements, and an inlined icon is in the SVG namespace, so the attribute no longer hides it. Script that toggles an icon needs an explicit `[hidden] { display: none }` rule, and a guard written as `instanceof HTMLElement` stops matching — an SVG element is not one.

One limitation is new to logos. About 400 `logos` icons draw with gradients, masks, or clip paths, which they reach by `id`. Each logo's ids are prefixed with its name, so two different logos never collide, but two copies of the same logo share them, and the browser draws both copies from the first. If that first copy is inside an element hidden with `display: none`, as in a collapsed mobile menu, every later copy renders blank (seen in Chromium). Keep a visible copy ahead of the hidden one, hide with `visibility: hidden` instead, or use CSS mode for a logo that appears more than once, since each mask image there is a document of its own. The same applies to `renderSvg` and [one module per icon](#one-module-per-icon).

Rewriting covers `.html`, `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`, `.svelte`, and `.astro`. An `.astro` file is rewritten in both halves, its frontmatter escaped as JavaScript and its template left as markup. `content` plays no part here — it feeds stylesheet generation, which this mode does not do.

The class has to be written out in full, as a literal string, for the plugin to find it: a name assembled at runtime, as in `` `ic-${name}` ``, cannot be seen at build time. In CSS mode such a class simply has no rule generated. In svg mode it is worse, because there is no stylesheet to fall back on and the element renders as nothing. Reach for [one module per icon](#one-module-per-icon) whenever the icon is chosen at runtime; it is the supported form for that, and it is the only one that works in either mode.

## One module per icon

For icons chosen at runtime, import a single icon instead of a family:

```ts
const { svg } = await import("virtual:@codenhub/icons/lucide/heart");
element.innerHTML = svg;
```

The module exports the rendered markup as `svg` and its default export, and the resolved icon as `icon`. Each import is its own module, so the bundler splits and tree-shakes at icon granularity. A name with no prefix — `virtual:@codenhub/icons/close` — resolves like any other unqualified name. An unknown icon fails the build rather than resolving to nothing.

## See also

- [Delivery methods](docs/delivery/index.md) — how the methods compare.
- [Frameworks](docs/frameworks/index.md) — Astro and SvelteKit build on this plugin.
- [JavaScript API](docs/javascript-api.md) — preparing a `registry` by hand.

<!-- Source: docs/delivery/postcss.md -->

# PostCSS plugin

```ts
import lucide from "@codenhub/icons/data/lucide";
import postcssIcons from "@codenhub/icons/postcss";

postcssIcons({
  content: ["./src/**/*.html"],
  families: [lucide],
  defaultPrefix: "lucide",
  injectBase: true,
  attribution: "auto",
});
```

It takes the same `families`, `defaultPrefix`, `prefix`, and `strokeWidth` options as the [Vite plugin](docs/delivery/vite.md). PostCSS has no asset pipeline, so `attribution` accepts `auto` and `off` only, and there is no `mode: "svg"`.

| Option          | Purpose                                                            |
| --------------- | ------------------------------------------------------------------ |
| `content`       | Files and globs to scan for icon classes.                          |
| `families`      | Families to resolve against. Nothing is bundled by default.        |
| `defaultPrefix` | Family an unqualified name resolves against.                       |
| `injectBase`    | Whether to append the base `.ic` rules. Defaults to `true`.        |
| `prefix`        | Class prefix. Defaults to `ic`.                                    |
| `strokeWidth`   | Default for stroke-based families whose classes carry no modifier. |
| `attribution`   | `auto` or `off`. See [Licensing](docs/licensing.md).                 |

## Scanning

Unlike the Vite plugin, this plugin has no view of the bundler's module graph. When it processes a stylesheet it scans that stylesheet's own text and every path in `content`, and nothing else. List globs that cover wherever your icon classes are written — components, templates, HTML — or only classes that literally appear in the CSS will resolve:

```ts
postcssIcons({
  content: ["./app/**/*.{tsx,jsx,mdx}", "./components/**/*.{tsx,jsx}"],
  families: [lucide],
  defaultPrefix: "lucide",
});
```

The plugin runs once per stylesheet it processes, so apply it to a single global stylesheet rather than to every CSS module.

## See also

- [Delivery methods](docs/delivery/index.md) — how the methods compare.
- [Frameworks: Next.js](docs/frameworks/nextjs.md) — the main reason to reach for PostCSS over Vite.

<!-- Source: docs/frameworks/index.md -->

# Framework setup

Every framework consumes `@codenhub/icons` through one of the [delivery methods](docs/delivery/index.md). This section covers the frameworks whose setup has a wrinkle worth spelling out; the rest follow their bundler.

| Framework                           | Recommended method                              | Notes                                                                                           |
| ----------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| [Astro](docs/frameworks/astro.md)                   | Plain CSS or Tailwind v4; Vite plugin with care | `.astro` is not auto-scanned by the Vite plugin — pass `content`.                               |
| [Next.js](docs/frameworks/nextjs.md)                | Tailwind v4 or PostCSS plugin                   | No Vite pipeline, so the Vite plugin does not apply.                                            |
| [SvelteKit](docs/frameworks/sveltekit.md)           | Vite plugin                                     | `.svelte` is auto-scanned; the cleanest plugin story.                                           |
| Vue, SolidStart, Nuxt, Remix (Vite) | Vite plugin                                     | Follow [SvelteKit](docs/frameworks/sveltekit.md); `.vue` is auto-scanned like `.svelte`.                        |
| Any framework                       | Plain CSS                                       | [Plain-CSS](docs/delivery/plain-css.md) family imports work everywhere that resolves package CSS. |

If your framework is not listed and it builds with Vite, follow the [SvelteKit](docs/frameworks/sveltekit.md) guide. If it builds with webpack or another PostCSS host, follow [Next.js](docs/frameworks/nextjs.md). If in doubt, [plain CSS](docs/delivery/plain-css.md) works anywhere at the cost of larger stylesheets.

<!-- Source: docs/frameworks/astro.md -->

# Astro

Astro builds on Vite, so all four [delivery methods](docs/delivery/index.md) are available. Two of them need no Astro-specific setup.

## Plain CSS (no configuration)

Import the base rules and a family stylesheet from a layout or a global stylesheet:

```css
/* src/styles/icons.css */
@import "@codenhub/icons";
@import "@codenhub/icons/lucide";
```

```astro
---
import "../styles/icons.css";
---
<i class="ic-lucide-heart" aria-hidden="true"></i>
```

Astro resolves the package's CSS exports directly. This is the least-effort path; its cost is stylesheet size and no stroke-width modifier. See [Plain CSS](docs/delivery/plain-css.md).

## Tailwind CSS v4 (no configuration)

With `@tailwindcss/vite` configured, add one import to your Tailwind entry:

```css
@import "tailwindcss";
@import "@codenhub/icons/tw";
```

Only the icons your markup uses are emitted. See [Tailwind CSS](docs/delivery/tailwind.md).

## Vite plugin

Add the plugin in `astro.config.mjs`:

```js
import lucide from "@codenhub/icons/data/lucide";
import icons from "@codenhub/icons/vite";
import { defineConfig } from "astro/config";

export default defineConfig({
  vite: {
    plugins: [
      icons({
        families: [lucide],
        defaultPrefix: "lucide",
        content: ["./src/**/*.{astro,md,mdx,html}"],
      }),
    ],
  },
});
```

Then import the generated stylesheet once, from a layout every page uses:

```astro
---
import "../styles/global.css";
import "virtual:icons.css";
---
```

Both lines are required, and this one is specific to Astro. Everywhere else the plugin injects the generated stylesheet into the HTML itself, from Vite's `transformIndexHtml` hook. A production `astro build` renders pages through Astro's own pipeline rather than as Vite HTML entries and does not put them through that hook, so the injection never reaches them and this import is what puts the mask rules on the page. Without it the build still succeeds, `@import "@codenhub/icons"` still supplies the base rules, and every icon renders as a solid square in its color.

The `content` entry is required, not optional. The plugin auto-scans the files Vite transforms for `.html`, `.js(x)`, `.ts(x)`, `.vue`, `.svelte`, and stylesheets — `.astro` is not in that set, so classes written in `.astro`, `.md`, or `.mdx` templates are only discovered through `content`. List those globs and keep `@import "@codenhub/icons";` in a stylesheet for the base rules.

Check that every class you write resolves in a family you registered. An icon name the registry cannot find is left without a rule and renders as a solid square. CSS mode reports nothing when that happens — in any host, not only in Astro — so the build stays green and the gap is visible only on the page. Inline SVG mode does warn, naming the class and the file.

## Inline SVG mode

`mode: "svg"` works in Astro and rewrites `<i class="ic-...">` tags in `.astro` files — in the template, in the frontmatter, and in a `<script>` block, each escaped for what it is. An icon tag written inside a _string_ in a template expression is the one place it is not handled, because `{cond && <i class="ic-x" />}` holds markup rather than a string and telling those apart needs a parser; put such a tag in the frontmatter or a component instead. Everything above about delivering the mask stylesheet stops applying: svg mode emits no per-icon rules, so neither the `virtual:icons.css` import nor `content` does anything here, and a class on any element the plugin does not rewrite — anything that is not an `<i>` tag — renders nothing, because there is no stylesheet behind it.

Keep `@import "@codenhub/icons"` even so. An inlined `<svg>` renders at `1em` on its own, but that base stylesheet is what lets `--ic-size` and the `ic-xs`–`ic-xl` size classes reach it. See [Vite plugin § Inline SVG mode](docs/delivery/vite.md#inline-svg-mode).

`.md` and `.mdx` are not rewritten. An icon class written in one of those reaches the page as a bare `<i>` element with no mask rule behind it, so it renders as a solid square with the base rules loaded, or as nothing without them. Nothing reports it, either: the unresolved-class warning only covers the files this mode rewrites, so such a page builds green and ships broken icons.

There are two ways through it. In `.mdx`, put the icon in a component and use the component from the markdown: the component is an `.astro`, `.jsx`, or `.tsx` file, all of which are rewritten. In plain `.md`, which cannot import a component, there is no such route — a site that writes icon classes directly into its markdown wants CSS mode, where `content` globs cover `.md` and `.mdx` like any other file.

## See also

- [Vite plugin](docs/delivery/vite.md) — full option reference and the scanning model.
- [Delivery methods](docs/delivery/index.md) — how the methods compare.

<!-- Source: docs/frameworks/nextjs.md -->

# Next.js

Next.js builds with webpack or Turbopack, not Vite, so `@codenhub/icons/vite` does not apply. Three methods do. All of them produce plain CSS, so the App Router, React Server Components, and streaming are unaffected.

## Tailwind CSS v4

If the project uses Tailwind v4 through `@tailwindcss/postcss`, add one import to your global stylesheet:

```css
/* app/globals.css */
@import "tailwindcss";
@import "@codenhub/icons/tw";
```

Only the icons your markup uses are emitted. See [Tailwind CSS](docs/delivery/tailwind.md).

## PostCSS plugin

Add the plugin to `postcss.config.mjs` and give it globs for wherever icon classes are written — it scans the stylesheet text and `content` only, with no view of the module graph:

```js
import lucide from "@codenhub/icons/data/lucide";
import icons from "@codenhub/icons/postcss";

export default {
  plugins: [
    icons({
      families: [lucide],
      defaultPrefix: "lucide",
      content: ["./app/**/*.{tsx,jsx,mdx}", "./components/**/*.{tsx,jsx}"],
    }),
  ],
};
```

Keep `@import "@codenhub/icons";` at the top of `app/globals.css` for the base rules. See [PostCSS plugin](docs/delivery/postcss.md).

## Plain CSS

The simplest option, at the cost of stylesheet size:

```css
/* app/globals.css */
@import "@codenhub/icons";
@import "@codenhub/icons/lucide";
```

See [Plain CSS](docs/delivery/plain-css.md).

## See also

- [Delivery methods](docs/delivery/index.md) — how the methods compare.
- [Concepts](docs/concepts.md) — name resolution and the markup forms.

<!-- Source: docs/frameworks/sveltekit.md -->

# SvelteKit

SvelteKit builds on Vite, and `.svelte` is one of the file types the Vite plugin auto-scans, so this is the cleanest plugin setup of any framework.

## Vite plugin

Add the plugin in `vite.config.ts` alongside `sveltekit()`:

```ts
import { sveltekit } from "@sveltejs/kit/vite";
import lucide from "@codenhub/icons/data/lucide";
import icons from "@codenhub/icons/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    sveltekit(),
    icons({
      families: [lucide],
      defaultPrefix: "lucide",
      content: ["./src/**/*.{svelte,html}"],
    }),
  ],
});
```

```css
/* src/app.css */
@import "@codenhub/icons";
```

Classes in `.svelte` components are picked up by the transform-time scan with no `content` entry at all. Listing `src/**/*.{svelte,html}` anyway makes the first paint correct in dev, before Vite has transformed those modules. `mode: "svg"` also works on `.svelte` files.

## Other methods

[Tailwind CSS v4](docs/delivery/tailwind.md) and [plain CSS](docs/delivery/plain-css.md) work in SvelteKit exactly as they do anywhere else. Vue, SolidStart, and other Vite-based frameworks follow this same setup — `.vue` is auto-scanned like `.svelte`.

## See also

- [Vite plugin](docs/delivery/vite.md) — full option reference and the scanning model.
- [Delivery methods](docs/delivery/index.md) — how the methods compare.

<!-- Source: docs/javascript-api.md -->

# JavaScript API

The plugins are built on a small set of exported helpers. Reach for them directly when you generate CSS or SVG outside a bundler, or when you need to prepare a registry the plugins can reuse.

```ts
import { generateIconSetCss, getIconCssProps, IconRegistry, renderSvg } from "@codenhub/icons";
import lucide from "@codenhub/icons/data/lucide";

const registry = new IconRegistry({ defaultPrefix: "lucide" });
registry.registerFamily(lucide);

const heart = registry.resolve("lucide:heart");
renderSvg(heart!, { strokeWidth: 1.5 });

const { css, families } = generateIconSetCss(["ic-lucide-heart", "ic-lucide-heart/1.5"], registry);

getIconCssProps("lucide:x", registry);
// { "--ic-uri": 'url("data:image/svg+xml;…")', "--ic-mask": "var(--ic-uri)" }
```

`generateIconSetCss` returns the families it drew from alongside the CSS, which is what lets a caller emit the right license notice. Every export, with its options, is listed in the [API reference](docs/reference/index.md).

## Generating CSS outside a bundler

The plugins are a scan followed by a generate. Run the two yourself to produce a stylesheet from a script:

```ts
import { generateIconSetCss, IconRegistry, renderAttributionBanner, scanFiles } from "@codenhub/icons";
import lucide from "@codenhub/icons/data/lucide";

const registry = new IconRegistry({ defaultPrefix: "lucide" });
registry.registerFamily(lucide);

const classes = scanFiles(["./src/**/*.html"]);
const { css, families } = generateIconSetCss(classes, registry);
const banner = renderAttributionBanner(families); // undefined when no family used owes a notice
const stylesheet = banner ? `${banner}\n${css}` : css;
```

`scanFiles` takes paths and glob patterns, and ignores a file it cannot read.

`generateFamilyCss` writes out one whole family instead, the way the package builds its own `@codenhub/icons/<family>` stylesheets.

## A background-image icon

The base rules render an icon as an element (`<i>` or `.ic`) or an inlined `<svg>`, and nothing else. When you need one as a `background-image` — a custom control, an input affix, a `::before` you own — resolve the `url()` yourself:

```ts
getIconMaskUrl("lucide:search", registry); // 'url("data:image/svg+xml;…")'
getIconCssProps("lucide:search", registry); // { "--ic-uri": …, "--ic-mask": "var(--ic-uri)" }
```

`getIconMaskUrl` also takes a string that begins with `<svg` and encodes it directly, skipping the registry — pass the element itself, with no leading whitespace or XML declaration. `getIconCssProps` gives the two custom properties an inline `style` attribute or a CSS-in-JS object needs; pair them with `background-image: var(--ic-uri)` or `mask-image: var(--ic-mask)` and your own sizing.

## Loading a family on demand

```ts
registry.registerLoader("phosphor", () => import("@codenhub/icons/data/phosphor"));

await registry.resolveAsync("phosphor:heart");
```

`resolve` is synchronous and sees only loaded families; `resolveAsync` loads the family first. Build-time consumers use the synchronous path, because a build knows its families before it starts.

## Third-party icon sets

`adoptIconifySet` converts an IconifyJSON-shaped document into this package's internal family contract:

```ts
import { adoptIconifySet } from "@codenhub/icons";

registry.registerFamily(adoptIconifySet(someIconifySet, { attribution: "notice" }));
```

Pass `attribution: "credit"` instead when the adopted set's license requires visible authorship credit, such as CC-BY.

Adopted sets are `extended` tier: the licensing promise this package makes covers the families it generates, not data a consumer supplies. See [Licensing](docs/licensing.md).

<!-- Source: docs/licensing.md -->

# Licensing and attribution

Every bundled, generated family ships the license text and attribution notice its artwork requires; those files travel with the package whatever a build does. A third-party set adopted through `adoptIconifySet` is not covered by that promise — bring your own licensing material for it.

## Attribution modes

What reaches your own output is the `attribution` option:

| Mode             | Behavior                                                                                                                                                                            |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto` (default) | Prepends a `/*! … */` banner to the generated CSS, naming only the families the build used. Vite's inline SVG mode, which has no stylesheet, emits `icons-attribution.txt` instead. |
| `file`           | Emits `icons-attribution.txt` as a build asset instead. Vite only.                                                                                                                  |
| `off`            | Emits nothing, and warns when a family used still requires a notice.                                                                                                                |

## How the notice travels

The vehicle differs by delivery method, because not every method can carry a comment:

| Method             | How the notice travels                                                                                    |
| ------------------ | --------------------------------------------------------------------------------------------------------- |
| Vite               | A `/*! … */` banner on the generated CSS, or `icons-attribution.txt` under `file` and in inline SVG mode. |
| PostCSS            | A `/*! … */` banner on the generated CSS.                                                                 |
| Tailwind `/tw`     | A `/*! … */` banner opening `dist/tw/index.css`, naming every family it resolves.                         |
| Tailwind `@plugin` | A `--ic-attribution-<family>` custom property on `:root`, one per family the build used.                  |
| Plain CSS          | A `/*! … */` banner opening each family stylesheet you imported.                                          |

`@import "@codenhub/icons/tw"` inlines a static file, so it carries an ordinary `/*! … */` banner. That file makes every bundled family resolvable except brand families, so the banner names the rest — a superset notice, which is always safe. Declaring the plugin yourself with `@plugin "@codenhub/icons/tailwind"` narrows resolution, and there the notice is a `--ic-attribution-<family>` custom property instead: Tailwind's plugin API builds declarations, not comments, and the property is emitted the first time a family produces a utility, so the output carries notices for the artwork it actually shipped and no other.

## Obligation levels

Obligations come in three levels, because permissive is not the same as free of obligation:

- `none` — public-domain dedications. Nothing is owed.
- `notice` — MIT, ISC, Apache-2.0. The notice must travel with distributed output, which `auto` and `file` handle for you.
- `credit` — CC-BY and similar. The author must be credited visibly.

Every bundled family except `simple-icons` and `logos` is `core` tier, meaning `none` or `notice`, so a default build satisfies every obligation automatically. A minifier configured with `legalComments: "none"` strips CSS comments, which is what `file` mode exists for.

A set adopted through `adoptIconifySet` is `extended` tier — you remain responsible for its license terms and attribution material. See [JavaScript API](docs/javascript-api.md).

## Brand families

`simple-icons` and `logos` are `brand` tier: one-color logos from [Simple Icons](https://simpleicons.org) and full-color logos from [gilbarbara/logos](https://github.com/gilbarbara/logos), both dedicated to the public domain under CC0. Neither owes a license notice, but a logo is a trademark, and the trademark is not the license: using one is subject to its owner's brand guidelines, which no build setting can satisfy and the package makes no promise about.

- Each is opt-in. Vite and PostCSS load only the families you pass. `@codenhub/icons/tw` never resolves brand families; a declared `@plugin "@codenhub/icons/tailwind"` resolves them only when its `families` option names them.
- Each family's notice line, wherever the build emits one, says that the marks are trademarks of their owners. `off` suppresses it without a warning, because no license requires it.
- Simple Icons records a separate license for some icons. Those whose own license places any obligation are left out, so no icon in the family carries a recorded license obligation: each is CC0, Unlicense, or has no license recorded upstream.
- Each icon renders in its brand colors by default, and `--ic-color` can recolor it; a full-color logo becomes a one-color silhouette. Owners' guidelines commonly allow a mark in its official color or in plain monochrome and restrict other colors; which recoloring a mark allows is the owner's call. See [Color](docs/concepts.md#color).
- Simple Icons removes icons when brand owners ask. An upgrade of the family across an upstream major version can remove names, and its changelog lists them.
- gilbarbara/logos publishes no releases, so `logos` is generated from a pinned commit, and any upgrade of it can remove or rename logos; its changelog lists them. A few upstream files whose markup cannot render as a self-contained icon, such as one embedding a bitmap, are left out, and the family's `ATTRIBUTION.md` names them.

<!-- Source: docs/troubleshooting.md -->

# Troubleshooting

Most failures here are silent: the build stays green and the page is where the gap shows. Work through the section that matches what you see.

## The icon renders as nothing

No base rule matched the element, so it has no size.

- **No base rules.** Plain CSS and a declared Tailwind `@plugin` need `@import "@codenhub/icons";` in a stylesheet. `@codenhub/icons/tw`, the Vite plugin's stylesheet, and the PostCSS plugin include the base rules already, the PostCSS plugin unless `injectBase` is `false`. Inline SVG mode needs no base rules to render; they add `--ic-size`, the size classes, and `--ic-color` for icons without a color of their own.
- **The element is not an `<i>` and has no `.ic`.** The base rules match `<i class="ic-…">`, or any element that also carries `.ic`. A `<span class="ic-lucide-heart">` matches neither.
- **An inlined logo whose first copy is hidden.** In inline SVG, two copies of the same `logos` icon share their gradient, mask, and clip-path ids, and a copy drawn after one inside a `display: none` element renders blank. See [Vite § Inline SVG mode](docs/delivery/vite.md#inline-svg-mode).

## The icon renders as a solid square

The base rules sized and colored the element, but no rule gave it a mask, so the whole box paints in the icon color.

- **The name does not exist in the family.** Names are upstream names: Material Symbols writes `check_circle`, Lucide writes `circle-check`, and Lucide has no `close` (it is `x`). See [Finding an icon's name](docs/concepts.md#finding-an-icons-name).
- **An unqualified name with no default family.** `ic-heart` resolves only where you set a default: `defaultPrefix` in the Vite and PostCSS plugins, `default:` in the Tailwind plugin, or the last family stylesheet imported in plain CSS. Write the qualified `ic-lucide-heart`, or set a default. See [Names](docs/concepts.md#names).
- **The family is not loaded.** The Vite and PostCSS plugins resolve only the families in `families`.
- **A brand icon through `@codenhub/icons/tw`.** `/tw` never resolves `simple-icons` or `logos`. Declare the plugin with `families` naming it. See [Tailwind CSS](docs/delivery/tailwind.md#configuring-the-plugin).
- **The class is assembled at runtime.** A plugin finds a class only when it is written out in full: `` `ic-${name}` `` is invisible at build time. Write each class literally, or load the icon as a [module](docs/delivery/vite.md#one-module-per-icon).
- **The class is in a file the plugin does not scan.** The Vite plugin scans the modules Vite transforms; `.astro`, `.md`, and `.mdx` need `content` globs. The PostCSS plugin scans only the stylesheet and `content`. See [Vite § Scanning](docs/delivery/vite.md#scanning) and [PostCSS § Scanning](docs/delivery/postcss.md#scanning).
- **Astro, production build only.** Import `virtual:icons.css` from a layout. See [Astro § Vite plugin](docs/frameworks/astro.md#vite-plugin).
- **Production only, with a Content Security Policy.** Versions up to 0.2.1 inlined the built stylesheet as a `<style>` tag, which a policy without `'unsafe-inline'` blocks. Later versions link a file instead. See [Vite § Scanning](docs/delivery/vite.md#scanning).
- **Inline SVG mode, on an element it does not rewrite.** `mode: "svg"` replaces `<i class="ic-…">` tags and emits no mask rules, so `.ic` on any other element has nothing behind it. The build warns, naming the class and the file. Under Astro, `.md` and `.mdx` are not rewritten, and nothing warns. See [Vite § Inline SVG mode](docs/delivery/vite.md#inline-svg-mode) and [Astro § Inline SVG mode](docs/frameworks/astro.md#inline-svg-mode).

## The stroke width does not change

- **Plain CSS.** Family stylesheets carry each icon at its authored width; `ic-lucide-heart/1.5` needs Tailwind, Vite, or PostCSS. See [Stroke width](docs/concepts.md#stroke-width).
- **A family drawn with fills.** Only `lucide` is stroke-based. Every other bundled family ignores the modifier.

## The color does not change

Set `--ic-color` on the icon or an ancestor, or `--<prefix>-color` under a custom class prefix. `color` reaches only icons without a color of their own: a brand icon keeps its brand color until `--ic-color` is set. A `background-color` you set on the icon element itself replaces its color, because the mask paints with the background. In inline SVG mode under a Content Security Policy that blocks `style` attributes, a brand icon keeps its brand color and ignores `--ic-color`. See [Color](docs/concepts.md#color).

## A brand icon is invisible in dark mode

Its brand color is near-black, as GitHub's, X's, and Vercel's are. Set `--ic-color` on it in your dark theme. See [Color](docs/concepts.md#color).

## The license notice is missing from the output

A minifier configured with `legalComments: "none"` strips `/*! … */` comments. Use `attribution: "file"` with Vite, or keep legal comments. See [Licensing](docs/licensing.md).

<!-- Source: docs/changelog/0.3.0.md -->

# 0.3.0

## Added

- `simple-icons`, a brand family of 3,254 logos such as `ic-simple-icons-github` and `ic-simple-icons-npm`, from [Simple Icons](https://simpleicons.org). It is opt-in: `@codenhub/icons/tw` never resolves it, and the other methods resolve it only when named, and its stylesheet carries only qualified names, so it never takes a bare name such as `ic-x` from another family. Icons that Simple Icons records under a license of their own are left out unless that license is one that places no obligation, CC0 or Unlicense. The marks are trademarks of their owners; see Licensing.
- `logos`, a brand family of 2,016 full-color logos such as `ic-logos-google` and `ic-logos-slack-icon`, from [gilbarbara/logos](https://github.com/gilbarbara/logos) under CC0-1.0, and its stylesheet `@codenhub/icons/logos`. It is opt-in in the same way as `simple-icons`, and its stylesheet likewise carries only qualified names. Multicolor marks keep their colors and gradients in CSS, inline SVG, and a Vite build, including under a Content Security Policy that blocks inline styles. Variants such as `-icon` and `-dark` are separate names. Two upstream files whose markup cannot render self-contained, `sinatra` and `waffle-icon`, are left out. About 80% of the logos are not square, mostly wordmarks, and render small in the square icon box. Inlined as SVG, two copies of one logo share its gradient, mask, and clip-path ids, so a copy shown after one hidden with `display: none` renders blank; see Vite, Inline SVG mode. [codenhub/codenhub#326](https://github.com/codenhub/codenhub/issues/326)
- Brand icons render in their brand color by default. `--ic-color`, set on an icon or any ancestor, overrides it: `:root { --ic-color: currentColor; }` makes every icon monochrome, and a per-icon value recolors one, turning a full-color logo into a one-color silhouette. In inline SVG mode under a Content Security Policy that blocks `style` attributes, a brand icon keeps its brand colors and `--ic-color` no longer reaches it. Several brand colors are near-black and need an override in a dark theme. See Concepts, Color.
- `IconData.color` and `ResolvedIcon.color` carry an icon's own color. `RenderSvgOptions.prefix` sets the class prefix whose `--<prefix>-color` overrides it.
- A Troubleshooting page, guidance on finding icon names, accessibility guidance, and a generated API reference.

## Changed

- **Breaking.** Node.js 24 or newer is required, up from 22. 24 is the only version line CI runs.
- **Breaking.** `escapeSelectorClass`, `generateIconCss`, `GenerateIconCssOptions`, and `renderSuppressedAttributionWarning` are no longer exported. They were the plugins' internals; `generateIconSetCss` and `generateFamilyCss` produce complete rules.
- **Breaking.** The base rules paint an icon with the `background` shorthand, `var(--ic-color, var(--ic-brand-paint, var(--ic-brand-color, currentColor)))`, instead of `background-color`, so a full-color logo can paint its own artwork. An element carrying `.ic` no longer keeps a `background-image` of its own. Nothing else changes for an icon without a color of its own.
- A Vite build in CSS mode with HTML entries links the generated stylesheet as a hashed CSS asset instead of inlining a `<style>` tag. A build without HTML entries, such as a library build, emits no stylesheet.
- **Breaking.** `drive_fusiontable` is gone from all six Material Symbols families, removed upstream in `@material-symbols/svg-400` 0.48.0. Material Symbols now has 3,929 icons per family, up from 3,899, and Lucide 2,159, up from 2,034 (`lucide-static` 1.59.0).
- The package is larger: the tarball unpacks to about 66 MB, up from 43 MB, mostly the `logos` artwork. Builds with the Tailwind plugin use more memory, since it loads every family. CSS from the plugins still carries only the icons a project uses.

## Fixed

- Material Symbols and Simple Icons icons rendered black when inlined through `mode: "svg"`, `renderSvg`, or a `virtual:@codenhub/icons/…` module, ignoring `color` and `--ic-color`. Their artwork sets no fill of its own; the rendered `<svg>` now carries `fill="currentColor"`.
- Every icon rendered blank in a production Vite build served with a Content Security Policy that does not allow inline styles. [codenhub/codenhub#318](https://github.com/codenhub/codenhub/issues/318)
- Each family's `LICENSE` and `ATTRIBUTION.md` were missing from the published package, though the notices and the `attribution: "off"` warning point to them. [codenhub/codenhub#323](https://github.com/codenhub/codenhub/issues/323)
- An icon inlined by `mode: "svg"` lost its icon class, so `--ic-size`, `--ic-color`, and the size classes did not reach it.
- `mode: "svg"` rewrote `<i data-class="ic-…">` as if it were `class`.
- The PostCSS plugin's generated rules carried no source file, so Vite warned on every build.
- Icon class names holding characters other than `.` and `/`, possible with a custom family or prefix, produced invalid selectors. They are now escaped as `CSS.escape` would.
- `mode: "svg"` broke a build, or changed the string, when an inlined icon landed in a string literal and its artwork held a backslash or a line break; when the tag sat in a double-quoted string inside a template literal's `${…}`; or, in an `.astro` template, when a `<script>` closed with a space before `>`.
- The codenhub.dev package list linked the demo to `icons.codenhub.dev` instead of `demo.codenhub.dev/icons/`.

<!-- Source: docs/changelog/0.2.1.md -->

# 0.2.1

## Changed

- Documentation status promoted from `experimental` to `active`. The README no longer carries the pre-1.0 instability warning.

<!-- Source: docs/changelog/0.2.0.md -->

# 0.2.0

## Added

- `@codenhub/icons/tw` for Tailwind v4: the base rules plus a plugin that generates icons as Tailwind matches them, so a project carries only what its markup used and ships no stylesheet. `@codenhub/icons/tailwind` exposes the plugin on its own, so `@plugin` can carry options.
- `@codenhub/icons/<family>` for a plain CSS pipeline: one complete family, every icon written out because nothing is there to narrow it. Each rule carries both the qualified and the bare selector, so import order picks the default family by plain cascade.
- A license banner on `@codenhub/icons/tw`, naming every bundled family. The configured `@plugin` path keeps its per-family `--ic-attribution-<family>` property, which is the most Tailwind's declaration-only plugin API allows.
- An `engines` field requiring Node 22. The scanner calls `fs.globSync`, stable only from Node 22, and the README already stated the requirement — without the field a consumer on Node 20 installed cleanly and then failed at build time with no warning.

## Changed

- **Breaking.** `@import "@codenhub/icons"` is now always the base rules and nothing else. The Vite plugin no longer substitutes the generated set at that line, nor deletes it in svg mode; the masks arrive through `virtual:icons.css` and the injected stylesheet instead. The same line previously meant two different things depending on configuration, and a pipeline without a plugin resolved it to base rules, rendered nothing, and said nothing.
- The Tailwind plugin reads its stroke width option as `stroke-width`. The camelCase `strokeWidth` still works for a JavaScript caller and wins when both are given.

## Fixed

- The `content` option expands glob patterns. It is documented as taking file paths or globs and the README example passes one, but the scanner only ever called `statSync` on each entry, so a pattern matched nothing and content scanning silently did not happen.
- The documented `@plugin` stroke width option reaches the plugin. Tailwind hands an option through exactly as written, but a CSS formatter lowercases a property name, so `strokeWidth` became `strokewidth` and was dropped — icons rendered at their authored width with no error and nothing in the output to explain it.
- The generated stylesheet is referenced as a module in dev rather than inlined as a `<style>` tag. The inlined tag was a snapshot taken before Vite had transformed any module, so a class in a file the plugin had not seen yet was missing from it and nothing could put it back. A build still inlines the tag, where modules are transformed before the HTML is.
