Welcome to Oshon · v1.0  ·  Now in public beta for enterprise teams Read the launch notes
MediaUpdatedFreeWCAG 2.2 AA

Icon Library

1,711-icon stroke catalog (lucide-static / ISC) · `currentColor`-themed · tree-shakable named exports · full browser at /icons.

Preview

Live preview
@oshon-ai/components
Full catalog — 1,718 stroke-style icons
1,718Named exports · tree-shakableBrowse full catalog →
Action · 14 examples
Plus
Minus
X
Check
Trash2
Download
Upload
Copy
Pencil
Save
RefreshCw
Send
Share2
Heart
Navigation · 14 examples
Search
Filter
Menu
House
ArrowLeft
ArrowRight
ArrowUp
ArrowDown
ChevronLeft
ChevronRight
ChevronUp
ChevronDown
ExternalLink
CornerDownRight
Status & feedback · 12 examples
CircleCheck
CircleAlert
CircleX
Info
TriangleAlert
ShieldCheck
ShieldAlert
Bug
Loader
CircleDot
Sparkles
Files & data · 14 examples
File
FileText
FilePlus
FileDown
FileUp
Folder
FolderOpen
FolderPlus
Database
Table2
TrendingUp
Communication · 12 examples
Bell
BellRing
Mail
MailOpen
MessageCircle
MessageSquare
Phone
PhoneCall
Video
Mic
AtSign
Hash
Chrome & user · 14 examples
User
Users
UserPlus
UserCheck
Settings
Calendar
CalendarDays
Clock
Lock
LockOpen
Key
LogIn
LogOut
Sizes — xs · s · m · l · mobile
12px
16px
20px
24px
32px
Color — inherits via `currentColor`
Set color on the parent — every stroke follows.
Import — tree-shakable named exports
import { Plus, ChevronDown, CircleAlert } from '@oshon-ai/components/icons';

<Plus size={20} />
<ChevronDown size={16} label="Toggle section" />
<CircleAlert size={24} style={{ color: 'var(--oshon-color-error-600)' }} />

Installation

Install the runtime packages:

pnpm
pnpm add @oshon-ai/components @oshon-ai/tokens @oshon-ai/primitives

Or scaffold the component source directly into your codebase (shadcn-style):

pnpm
pnpm dlx @oshon-ai/cli add icons

Wire the tokens into your Tailwind v4 stylesheet:

css
/* app/globals.css */
@import 'tailwindcss';
@import '@oshon-ai/tokens/css';
@import '@oshon-ai/tokens/tailwind';

New here? Walk through the full setup — prereqs, theming, your first render.

Usage

Import the component and render it. Every component supports the standard tier, size, and disabled props where applicable.

tsx
'use client';
import { IconLibrary } from '@oshon-ai/components';

export default function Example() {
  return <IconLibrary />;
}

Styling

Three layers of customization, in order of escape-hatch strength: className overrides → data-attribute targeting → CSS custom properties.

Passing Tailwind classes

Every Oshon component accepts a className prop merged AFTER the component's default classes. Use it to override spacing, color, or size without forking the component.

tsx
<IconLibrary
  className="ring-2 ring-offset-2 ring-blue-500"
/>

Data attributes

Oshon components expose their internal state as data-oshon-* attributes so you can target them from CSS without coupling to internal class names. The most common attributes are listed below — see the component's source for the full set.

AttributeValuesDescription
data-oshon-sizexs · s · m · l · mobileVisual size axis. Mirrors the `size` prop.
data-oshon-tierprimary · secondary · tertiaryVisual emphasis tier (Button family). Mirrors the `tier` prop.
data-oshon-stateenabled · active · error · disabledComponent surface state. Set automatically based on props.
data-disabledtrue · (omitted)Set when `disabled` is true. Pair with `:disabled` CSS for native input components.
data-stateopen · closed · checked · unchecked · …Radix-derived state for overlay components (Dialog, Tabs, Toggle, etc.).
css
/* Target the secondary tier specifically */
[data-oshon-tier="secondary"] {
  --oshon-color-primary-700: var(--my-brand-color);
}

Interactive states

Every interactive component supports the standard CSS pseudo- classes plus Tailwind's state variants. Focus rings always use :focus-visible so keyboard users see them but mouse users don't.

  • :hover / hover:* — pointer hover
  • :focus-visible / focus-visible:* — keyboard focus
  • :active / active:* — pressed
  • :disabled / disabled:* — set via the disabled prop

Anatomy

The named regions a consumer composes when rendering this component. Each is documented separately so you can target keyboard nav, ARIA labels, and slot props with precision.

size

Width + height in px. Default 16. Stroke width does not scale automatically — consumers wanting a thicker stroke at larger sizes should pass `strokeWidth={1.5}` explicitly.

label

Accessible name. When set, the icon becomes `role="img"` + `aria-label="…"`. Omit for decorative icons sitting next to labelled text.

color

Inherited via `currentColor`. Set `color` on the parent (or pass `style={{ color: "var(--oshon-color-primary-700)" }}` on the icon) and the strokes follow. This is how icons inherit Oshon brand tokens without baking palette references into the SVG paths.

Keyboard

Icons are presentational SVG elements — they do not enter the tab order. Default behavior is `aria-hidden="true"` so screen readers skip purely decorative glyphs. Pass `label="…"` to promote an icon to a labelled image — the component renders `role="img"` + `aria-label="…"` and the icon is announced as part of the accessibility tree.

Accessibility

Every Oshon component ships axe-clean. We test in CI on every PR and publish the audit log per component.

WCAG level
2.2 AA
Screen readers tested
VoiceOver (macOS), NVDA (Windows)
Last axe audit
2026-05-20

Do / Don't

✓ Do

Tree-shakable named import
import { Plus } from '@oshon-ai/components/icons';
<Plus size={20} />
Inherit color from a parent button
<ButtonHug tier="primary" style={{ color: 'var(--oshon-color-on-primary)' }}>
  <Plus />
  Add member
</ButtonHug>
Labelled icon (standalone, no surrounding text)
<Trash label="Delete row" size={20} onClick={onDelete} />
Dynamic name (defeats tree-shaking — use sparingly)
import { Icon, type IconName } from '@oshon-ai/components/icons';
const names: IconName[] = ['check', 'x', 'info'];
names.map(n => <Icon key={n} name={n} />)

✗ Don't

Setting `color` on the icon's `style.fill` / `style.stroke`
<Plus style={{ fill: 'red', stroke: 'red' }} />

The icon's paths use `stroke="currentColor"`. Setting `fill` does nothing (the stroke is the visible line), and overriding `stroke` directly bypasses the `currentColor` inheritance the whole color system relies on. Use `style={{ color }}` or `className="text-[var(--oshon-color-primary-700)]"` so consumers can theme icons via the parent.

Using `<Icon name="..." />` for static icons
<Icon name="plus" />

The dynamic `<Icon>` pulls in ICON_NAMES which references every icon — bundlers can't tree-shake the unused ones. For static usage import the named component directly (`import { Plus }`). Reserve `<Icon name>` for data-driven cases.

Marking an icon as a labelled image when it sits next to text
<button><Trash label="Delete" /> Delete</button>

Screen readers will announce "Delete Delete" — once from the `role="img"` aria-label, once from the button text. Decorative icons (the default `aria-hidden="true"` behavior) inherit accessibility from the surrounding text. Only set `label` when the icon stands alone.

Design rationale

Tree-shaking is the dominant constraint — a real icon library lives or dies on bundle impact. Each glyph is its own named React component with no shared mutable state so esbuild / Rollup / Webpack can drop unused icons. The shared `IconBase` is intentionally tiny — just the `<svg>` shell + a11y wiring — so importing one icon pays for ~200 bytes of base plus the icon's own ~50-300 bytes of path data. Color inheritance via `currentColor` (instead of a `color` prop) keeps the API minimal and lets consumers theme icons through normal CSS cascade. The catalog is generated from lucide-static — 1,960 icons in v1.16.0 — by `scripts/generate-from-lucide.mjs`. Lucide is the modern shadcn / Vercel / HeroUI default; its 2-px stroke, 24×24 viewBox, and round caps + joins match `IconBase` exactly. The generator output is deterministic (same Lucide version → byte-identical files) and the public API is unchanged, so swapping to a Figma DS 3.1 export pipeline in a future release is a generator-side refactor with no consumer impact.