# Icon

## Usage

`Icon` draws one icon from an Iconify collection. Pass the name. You do not import an icon component for each glyph.

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
</script>

<template>
  <Icon name="carbon:settings" />
</template>
```

The name is `collection:icon`, the same id Iconify uses. `carbon-settings` is accepted too, once the `carbon` collection is registered. A collection whose own name contains a hyphen, such as `material-symbols`, should keep the colon: `material-symbols:home`.

Nothing in `@astryx-vue/core` bundles those drawings. Install the collection you actually use, then let the Vite plugin pull in only the icons your source names:

```bash
pnpm add -D @iconify-json/carbon
```

```ts
import { astryxIcons } from '@astryx-vue/core/icons'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [vue(), astryxIcons()],
})
```

`astryxIcons()` reads `@iconify-json/*` from the project's dependencies. A static `name="carbon:settings"` becomes a tiny module that registers that one icon. The rest of the Carbon set stays out of the bundle. Another set is the same step: install `@iconify-json/lucide` and write `name="lucide:settings"`. UnoCSS is not involved.

The plugin can only see names written in source. When the name is chosen at runtime, register the collection yourself. That ships the whole set:

```ts
import { addCollection } from '@astryx-vue/core/Icon'
import carbon from '@iconify-json/carbon/icons.json'

addCollection(carbon)
```

Call `addCollection` before the icon renders, usually from the app entry. `addIcon('carbon:settings', data)` registers a single icon the same way.

Leave `size` off and the icon is `1em` square, painted with `currentColor`, so it follows the text around it. [Button](/en/components/button)'s `icon` and `end` slots, [IconButton](/en/components/icon-button), and a field's start and end slots already size that box. Put `Icon` in the slot and do not set `size` again.

```vue
<Button label="Settings">
  <template #icon>
    <Icon name="carbon:settings" />
  </template>
</Button>
```

A decorative icon is `aria-hidden`. Set `label` only when the icon is the only thing that carries the meaning, and the parent control does not already have a name.

`icon` still accepts a CSS class or your own component. Navigation `iconClass` is still a class. Those paths do not require UnoCSS either.

## Semantic names

Inside the components, a name is the job, not a file in one collection. Search, close, check, and the downward chevron are that kind of name. Each one points at the Lucide icon that does the same job. Until that collection is registered, the built-in glyph stays, so the icon is not a blank box.

To change the set, point those names at Iconify ids on the theme. A theme that extends another one only lists the names it changes. A direct icon still uses `name`, and that wins over a semantic name.

```vue
<script setup lang="ts">
import { defineTheme, Theme } from '@astryx-vue/core/Theme'
</script>

<template>
  <Theme
    :theme="defineTheme({
      name: 'withCarbon',
      icons: { search: 'carbon:search', close: 'carbon:close' },
    })"
  >
    <slot />
  </Theme>
</template>
```

The full list is on the props page. Read the current id with `useSemanticIcon`, exported from the same entry as `Icon`.

### Anatomy

- **Glyph**: Visual symbol rendered for the selected icon.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use one collection inside a view. Mixing outline and filled drawings in the same row makes the row look unfinished. |
| Do | Leave `size` unset inside a button or a field slot. The slot already decides the box, and `1em` fills it. |
| Do | Set `label` when the icon stands alone and nothing next to it says what it means. |
| Do | Install only the collections you reference. The plugin bundles the icons you name, not the whole set. |
| Don't | Import a component per icon. `name` is the API; the plugin does the lookup. |
| Don't | Set `label` on an icon inside a button or a link that already has a name. The name would be announced twice. |
| Don't | Rely on a name that is concatenated at runtime without `addCollection` or `addIcon`. The plugin never sees it, and the icon renders empty. |

## Examples

### Basic

carbon:search at the md size, then the same settings icon with no size, sitting in body text and in large text. Omit size when the icon should match the line.

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <VStack :gap="4" h-align="start">
    <Icon name="carbon:search" color="primary" size="md" />
    <Text>
      <Icon name="carbon:settings" /> Settings
    </Text>
    <Text size="lg">
      <Icon name="carbon:settings" /> Settings
    </Text>
  </VStack>
</template>
```

### Sizes

xsm is 12px, sm is 16px, md is 20px, and lg is 24px, at a 16px root.

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <HStack :gap="4">
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="xsm" />
      <Text type="supporting">
        xsm
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="sm" />
      <Text type="supporting">
        sm
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="md" />
      <Text type="supporting">
        md
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" size="lg" />
      <Text type="supporting">
        lg
      </Text>
    </VStack>
  </HStack>
</template>
```

### Semantic colors

Semantic colors. primary, secondary, tertiary, and disabled follow the text ramp. accent, success, error, and warning carry meaning.

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'
</script>

<template>
  <HStack :gap="4" wrap="wrap">
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:search" color="primary" />
      <Text type="supporting">
        primary
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:menu" color="secondary" />
      <Text type="supporting">
        secondary
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:information" color="tertiary" />
      <Text type="supporting">
        tertiary
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:time" color="disabled" />
      <Text type="supporting">
        disabled
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:calendar" color="accent" />
      <Text type="supporting">
        accent
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:checkmark-filled" color="success" />
      <Text type="supporting">
        success
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:error" color="error" />
      <Text type="supporting">
        error
      </Text>
    </VStack>
    <VStack :gap="1" h-align="center">
      <Icon name="carbon:warning" color="warning" />
      <Text type="supporting">
        warning
      </Text>
    </VStack>
  </HStack>
</template>
```

### Palette

Palette colors for grouping. They do not report status.

```vue
<script setup lang="ts">
import type { IconColor } from '@astryx-vue/core/Icon'
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const colors = [
  'blue',
  'red',
  'green',
  'gray',
  'cyan',
  'teal',
  'yellow',
  'orange',
  'pink',
  'purple',
] as const satisfies readonly IconColor[]
</script>

<template>
  <HStack :gap="4" wrap="wrap">
    <VStack v-for="color in colors" :key="color" :gap="1" h-align="center">
      <Icon name="carbon:search" :color="color" />
      <Text type="supporting">
        {{ color }}
      </Text>
    </VStack>
  </HStack>
</template>
```

### Status

A status line keeps the icon and a text label. The icon is decorative because the text already says what happened.

```vue
<script setup lang="ts">
import type { IconColor } from '@astryx-vue/core/Icon'
import { Icon } from '@astryx-vue/core/Icon'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const statuses: { name: string, color: IconColor, label: string }[] = [
  { name: 'carbon:checkmark-filled', color: 'success', label: 'Deployed successfully' },
  { name: 'carbon:warning', color: 'warning', label: 'Build has warnings' },
  { name: 'carbon:error', color: 'error', label: 'Pipeline failed' },
  { name: 'carbon:information', color: 'accent', label: 'New version available' },
]
</script>

<template>
  <VStack :gap="3">
    <HStack v-for="status in statuses" :key="status.label" align="center" :gap="2">
      <Icon :name="status.name" :color="status.color" size="sm" />
      <Text type="body">
        {{ status.label }}
      </Text>
    </HStack>
  </VStack>
</template>
```

### In a button

Icon in a Button start slot, a Button end slot, an IconButton, and a text field's start and end slots. No size is set; each slot owns the box.

```vue
<script setup lang="ts">
import { Button } from '@astryx-vue/core/Button'
import { Icon } from '@astryx-vue/core/Icon'
import { IconButton } from '@astryx-vue/core/IconButton'
import { HStack, VStack } from '@astryx-vue/core/Stack'
import { TextInput } from '@astryx-vue/core/TextInput'
import { ref } from 'vue'

const email = ref('')
</script>

<template>
  <VStack :gap="4" h-align="start">
    <HStack :gap="3" wrap="wrap">
      <Button label="Settings" variant="primary">
        <template #icon>
          <Icon name="carbon:settings" />
        </template>
      </Button>
      <Button label="Continue" variant="secondary">
        <template #end>
          <Icon name="carbon:chevron-right" />
        </template>
      </Button>
      <IconButton label="Search">
        <Icon name="carbon:search" />
      </IconButton>
    </HStack>
    <div style="width: 280px; max-width: 100%">
      <TextInput v-model="email" label="Email" placeholder="ada@example.com">
        <template #icon>
          <Icon name="carbon:email" />
        </template>
        <template #end>
          <Icon name="carbon:view" />
        </template>
      </TextInput>
    </div>
  </VStack>
</template>
```

### Swap the drawings with the theme

The same semantic names. The first row is the built-in glyphs. The next two rows point search, close, check, and the downward chevron at Carbon and Heroicons.

```vue
<script setup lang="ts">
import { Icon } from '@astryx-vue/core/Icon'
import { defineTheme, Theme } from '@astryx-vue/core/Theme'

const names = ['search', 'close', 'check', 'chevronDown'] as const

const carbon = defineTheme({
  name: 'withCarbon',
  icons: {
    search: 'carbon:search',
    close: 'carbon:close',
    check: 'carbon:checkmark',
    chevronDown: 'carbon:chevron-down',
  },
})

const hero = defineTheme({
  name: 'hero-icons',
  icons: {
    search: 'heroicons:magnifying-glass-20-solid',
    close: 'heroicons:x-mark-20-solid',
    check: 'heroicons:check-20-solid',
    chevronDown: 'heroicons:chevron-down-20-solid',
  },
})
</script>

<template>
  <div class="icon-theme-board">
    <div class="icon-theme-row">
      <Icon v-for="name in names" :key="name" :icon="name" size="lg" />
      <span>Built in</span>
    </div>
    <Theme :theme="carbon">
      <div class="icon-theme-row">
        <Icon v-for="name in names" :key="name" :icon="name" size="lg" />
        <span>Carbon</span>
      </div>
    </Theme>
    <Theme :theme="hero">
      <div class="icon-theme-row">
        <Icon v-for="name in names" :key="name" :icon="name" size="lg" />
        <span>Heroicons</span>
      </div>
    </Theme>
  </div>
</template>
```

## Accessibility

- With no `label`, the root is `aria-hidden="true"`. The SVG is hidden as well.
- A non-empty `label` sets `role="img"` and `aria-label`, and removes `aria-hidden`. Use it for a status glyph that has no adjacent text.
- An empty `label` stays decorative.
- Do not set `label` when a parent button or link already names the control.
- The drawing uses `currentColor`. A color that is only a hex on the element will not follow the theme; use the `color` prop.

## Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | — | Iconify id, `collection:icon`. `carbon-settings` is the same as `carbon:settings` after the carbon collection is registered. Hyphenated collection names should keep the colon. |
| `icon` | `string \| Component` | — | A semantic name, a CSS icon class, or your own component. A semantic name resolves to an Iconify id from the nearest theme, and falls back to the built-in glyph until that icon is registered. Ignored when `name` is set. |
| `color` | `'inherit' \| 'primary' \| 'secondary' \| 'tertiary' \| 'disabled' \| 'accent' \| 'success' \| 'error' \| 'warning' \| 'blue' \| 'cyan' \| 'green' \| 'gray' \| 'orange' \| 'pink' \| 'purple' \| 'red' \| 'teal' \| 'yellow'` | `'inherit'` | Sets the color the icon's `currentColor` resolves to. `inherit` keeps the surrounding text color. |
| `size` | `'xsm' \| 'sm' \| 'md' \| 'lg'` | — | 12px, 16px, 20px, or 24px at a 16px root. When omitted, a `name` icon is `1em`. Any other icon falls back to `md` unless a parent provides a size. |
| `label` | `string` | — | Accessible name for a meaningful icon. Sets `role="img"` and `aria-label`. Omit it for decoration (`aria-hidden="true"`). An empty string stays decorative. |

## Slots

- Default: SVG written inline, used when neither <code>name</code> nor <code>icon</code> is set.

## Registration

| Name | From | Description |
| --- | --- | --- |
| `astryxIcons()` | `@astryx-vue/core/icons` | Vite plugin. Scans static names and bundles only those icons from installed `@iconify-json/*` packages. Pass `collections` to limit which prefixes are read. |
| `addIcon(name, data)` | `@astryx-vue/core/Icon` | Register one icon. `name` is `collection:icon`. `data.body` is the SVG inner markup. |
| `addCollection(collection)` | `@astryx-vue/core/Icon` | Register an `@iconify-json` document. The whole collection is included in the bundle. |

## Semantic names

`defineTheme({ icons })` overrides by name. Entries merge shallowly with the theme this one extends, and this theme wins. A name that is not listed keeps the default id. The collection has to be registered, or the built-in glyph stays.

| Name | Default Iconify id |
| --- | --- |
| `close` | `lucide:x` |
| `chevronDown` | `lucide:chevron-down` |
| `chevronLeft` | `lucide:chevron-left` |
| `chevronRight` | `lucide:chevron-right` |
| `chevronsLeft` | `lucide:chevrons-left` |
| `chevronsRight` | `lucide:chevrons-right` |
| `check` | `lucide:check` |
| `success` | `lucide:circle-check` |
| `error` | `lucide:circle-x` |
| `warning` | `lucide:triangle-alert` |
| `info` | `lucide:info` |
| `calendar` | `lucide:calendar` |
| `clock` | `lucide:clock` |
| `externalLink` | `lucide:external-link` |
| `menu` | `lucide:menu` |
| `moreHorizontal` | `lucide:ellipsis` |
| `search` | `lucide:search` |
| `upload` | `lucide:upload` |
| `arrowUp` | `lucide:arrow-up` |
| `arrowDown` | `lucide:arrow-down` |
| `arrowsUpDown` | `lucide:arrow-up-down` |
| `funnel` | `lucide:funnel` |
| `eyeSlash` | `lucide:eye-off` |
| `viewColumns` | `lucide:columns-3` |
| `copy` | `lucide:copy` |
| `checkDouble` | `lucide:check-check` |
| `wrench` | `lucide:wrench` |
| `stop` | `lucide:square` |
| `microphone` | `lucide:mic` |
| `image` | `lucide:image` |

These are the **shared names**. Every component draws from the same set. The `IconRegistry` type describes a complete registry that draws every shared name, and `upload` is required like the rest. A theme that supplies a whole icon set must give `upload` its own upload drawing instead of reusing `arrowUp`. Redrawing `upload` leaves the `arrowUp` used by sortable tables and the send button alone.

An icon that only one component draws does not become a shared name. It gets a namespaced key, `component:role`, typed as `NamespacedIconName`. There is one today:

| Key | Used by |
| --- | --- |
| `numberInput:stepperDown` | NumberInput's step buttons. A small down chevron; the increment button flips it. |

A theme overrides it through the same `icons` map: `defineTheme({ icons: { 'numberInput:stepperDown': 'carbon:chevron-down' } })`. Without an override the built-in glyph is drawn. Adding a namespaced key never adds a required entry to `IconRegistry`.

`useSemanticIcon(name)` comes from `@astryx-vue/core/Icon` and from the package root. An unknown string with no theme entry returns empty, so the caller can still treat it as a CSS class.

## Theme

Color follows the icon tokens. A theme replaces one by setting the variable on the theme root.

```css
.my-theme {
  --astryx-icon-accent-foreground: #0074e2;
}
```

- `--astryx-icon-primary-foreground`
- `--astryx-icon-secondary-foreground`
- `--astryx-icon-disabled-foreground`
- `--astryx-icon-accent-foreground`
- `--astryx-icon-success-foreground`
- `--astryx-icon-error-foreground`
- `--astryx-icon-warning-foreground`
- `--astryx-icon-blue-foreground`, and the same pattern for cyan, green, gray, orange, pink, purple, red, teal, and yellow.

## Accessibility

The root is a `<span>`. Without `label` it is `aria-hidden="true"`. With a non-empty `label` it is `role="img"` and that string is the accessible name. The SVG inside is always `aria-hidden`.

`class` and `style` fall through to the root. `name` does not: it selects the icon.
