# ThemeIcon (/docs/theme-icon)





`ThemeIcon` is a decorative, non-interactive icon glyph — the same size/radius scales as [`ActionIcon`](/docs/action-icon), but a deliberately different look. It renders a plain `div` (no `disabled`, `loading`, or click handling), and every variant is a muted surface: just enough contrast to sit above the page, never a vivid CTA-style fill. Reach for it wherever a themed icon glyph decorates content: feature callouts, list-item glyphs, empty states.

```tsx
import { ThemeIcon } from "@42-central/ui-react/theme-icon";
import { Search } from "lucide-react";

<ThemeIcon color="neutral" variant="filled" radius="lg">
  <Search />
</ThemeIcon>;
```

The icon is auto-sized by the parent's `size` variant — drop in a bare `lucide-react` icon and you'll never need to set `size={n}` on it.

## Playground [#playground]

<story.Playground />

## Variants [#variants]

Five muted surfaces — none of them read as a button. `filled` is a tinted surface with a border a shade darker, for a raised card feel; `light` is the same tint without the border; `outline` and `subtle` stay progressively quieter.

<Preview align="center" code="&#x22;import { Star } from 'lucide-react';\n\n<ThemeIcon variant=\&#x22;filled\&#x22;><Star /></ThemeIcon>\n<ThemeIcon variant=\&#x22;light\&#x22;><Star /></ThemeIcon>\n<ThemeIcon variant=\&#x22;outline\&#x22;><Star /></ThemeIcon>\n<ThemeIcon variant=\&#x22;subtle\&#x22;><Star /></ThemeIcon>\n<ThemeIcon variant=\&#x22;default\&#x22;><Star /></ThemeIcon>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <ThemeIcon variant="filled">
      <Star />
    </ThemeIcon>

    <ThemeIcon variant="light">
      <Star />
    </ThemeIcon>

    <ThemeIcon variant="outline">
      <Star />
    </ThemeIcon>

    <ThemeIcon variant="subtle">
      <Star />
    </ThemeIcon>

    <ThemeIcon variant="default">
      <Star />
    </ThemeIcon>
  </div>
</Preview>

## Colors [#colors]

Every variant (except `default`) accepts any color from the shared palette.

<Preview align="center" code="&#x22;<ThemeIcon variant=\&#x22;filled\&#x22; color=\&#x22;neutral\&#x22; radius=\&#x22;lg\&#x22;><Search /></ThemeIcon>\n<ThemeIcon variant=\&#x22;filled\&#x22; color=\&#x22;brand\&#x22;><Heart /></ThemeIcon>\n<ThemeIcon variant=\&#x22;light\&#x22;  color=\&#x22;blue\&#x22;><Star /></ThemeIcon>\n<ThemeIcon variant=\&#x22;outline\&#x22; color=\&#x22;teal\&#x22;><Settings /></ThemeIcon>\n<ThemeIcon variant=\&#x22;filled\&#x22; color=\&#x22;red\&#x22;><Trash2 /></ThemeIcon>&#x22;">
  <div className="flex flex-col gap-3">
    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ThemeIcon key={c} variant="filled" color={c}>
                <Star />
              </ThemeIcon>
            ))}
    </div>

    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ThemeIcon key={c} variant="light" color={c}>
                <Star />
              </ThemeIcon>
            ))}
    </div>

    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ThemeIcon key={c} variant="outline" color={c}>
                <Star />
              </ThemeIcon>
            ))}
    </div>

    <div className="flex flex-wrap gap-2">
      {COLORS.map((c) => (
              <ThemeIcon key={c} variant="subtle" color={c}>
                <Star />
              </ThemeIcon>
            ))}
    </div>
  </div>
</Preview>

## Sizes [#sizes]

Five sizes from `xs` to `xl` — identical footprint to `ActionIcon` at the same `size`, so the two drop into the same layouts without a visual mismatch.

<Preview align="center" code="&#x22;<ThemeIcon size=\&#x22;xs\&#x22;><Star /></ThemeIcon>\n<ThemeIcon size=\&#x22;sm\&#x22;><Star /></ThemeIcon>\n<ThemeIcon size=\&#x22;md\&#x22;><Star /></ThemeIcon>\n<ThemeIcon size=\&#x22;lg\&#x22;><Star /></ThemeIcon>\n<ThemeIcon size=\&#x22;xl\&#x22;><Star /></ThemeIcon>&#x22;">
  <div className="flex flex-wrap items-end gap-3">
    <ThemeIcon size="xs">
      <Star />
    </ThemeIcon>

    <ThemeIcon size="sm">
      <Star />
    </ThemeIcon>

    <ThemeIcon size="md">
      <Star />
    </ThemeIcon>

    <ThemeIcon size="lg">
      <Star />
    </ThemeIcon>

    <ThemeIcon size="xl">
      <Star />
    </ThemeIcon>
  </div>
</Preview>

## Radius [#radius]

Independent radius scale, including `full` for circular glyphs.

<Preview align="center" code="&#x22;<ThemeIcon radius=\&#x22;xs\&#x22;><Star /></ThemeIcon>\n<ThemeIcon radius=\&#x22;sm\&#x22;><Star /></ThemeIcon>\n<ThemeIcon radius=\&#x22;md\&#x22;><Star /></ThemeIcon>\n<ThemeIcon radius=\&#x22;lg\&#x22;><Star /></ThemeIcon>\n<ThemeIcon radius=\&#x22;xl\&#x22;><Star /></ThemeIcon>\n<ThemeIcon radius=\&#x22;full\&#x22;><Star /></ThemeIcon>&#x22;">
  <div className="flex flex-wrap items-center gap-3">
    <ThemeIcon radius="xs">
      <Star />
    </ThemeIcon>

    <ThemeIcon radius="sm">
      <Star />
    </ThemeIcon>

    <ThemeIcon radius="md">
      <Star />
    </ThemeIcon>

    <ThemeIcon radius="lg">
      <Star />
    </ThemeIcon>

    <ThemeIcon radius="xl">
      <Star />
    </ThemeIcon>

    <ThemeIcon radius="full">
      <Star />
    </ThemeIcon>
  </div>
</Preview>

## Polymorphic with `asChild` [#polymorphic-with-aschild]

Same Ark UI polymorphism as `ActionIcon`. Wrap a link when the icon itself needs to be clickable — the kit's [focus ring](/docs/focus) comes along, so the composed element is keyboard-visible. For the common "icon-only trigger" case, reach for [`ActionIcon`](/docs/action-icon) instead, which adds the rest of the wiring (`aria-label` requirements, disabled and loading states) that a real trigger needs.

```tsx
import Link from "next/link";
import { User } from "lucide-react";

<ThemeIcon asChild variant="subtle" color="neutral">
  <Link href="/profile" aria-label="Open profile">
    <User />
  </Link>
</ThemeIcon>;
```

## API [#api]

<TypeTable
  type="{
  variant: {
    type: &#x22;'filled' | 'light' | 'outline' | 'subtle' | 'default'&#x22;,
    default: &#x22;'default'&#x22;,
    description:
      &#x22;Visual treatment. All variants are muted surfaces — 'filled' adds a border for a raised, card-like read.&#x22;,
  },
  color: {
    type: &#x22;'brand' | 'neutral' | 'red' | 'orange' | 'yellow' | 'green' | 'teal' | 'cyan' | 'blue' | 'purple' | 'pink' | (string & {})&#x22;,
    description:
      'Accent palette. Custom values are accepted as long as a matching [data-color=&#x22;…&#x22;] block exists in theme.css.',
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: &#x22;Square dimensions. Matches ActionIcon at the same size.&#x22;,
  },
  radius: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'&#x22;,
    default: &#x22;'md'&#x22;,
    description: &#x22;Corner radius. Use 'full' for circular glyphs.&#x22;,
  },
  asChild: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Render children as the root element (Ark UI polymorphism).&#x22;,
  },
  testId: {
    type: &#x22;string&#x22;,
    description:
      &#x22;Renders as `data-testid` on the rendered `div`. See [Testing](/docs/testing).&#x22;,
  },
}"
/>

All other props are forwarded to the underlying `div` (or `asChild` element).

## When to use which [#when-to-use-which]

* Reach for `ThemeIcon` for a **decorative** themed icon glyph — nothing to click. Composed onto a link with `asChild` it still focuses correctly.
* Reach for [`ActionIcon`](/docs/action-icon) when the icon is an **interactive trigger** — it adds `aria-label` requirements, disabled states, and loading support that `ThemeIcon` intentionally omits.
* Reach for [`Badge`](/docs/badge) for an inline status chip or counter — `ThemeIcon` is a square, icon-first display, not a text label.
