Proper UI Icons are a clean, consistent line icon set built for modern interfaces, distributed as tree-shakeable React components.
Proper UI Icons are a clean, consistent, neutral icon library crafted for modern UI design, available as tree-shakeable React components under @properui/icons. Every component spec in this library references icons by these exact export names, so the set you install here is the same one every component's iconLeading / iconTrailing prop expects.
@properui/icons is not a package on npm. It is the name the component source imports, resolved to @untitledui/icons (MIT) through an npm alias — so that package's README and its 1,100+ icons apply directly here, and the import name stays stable if the underlying package ever changes.
Installation
npm install @properui/icons@npm:@untitledui/icons@^0.0.22
pnpm add @properui/icons@npm:@untitledui/icons@^0.0.22
The name@npm:real@range form is an npm alias — npm, pnpm, yarn and bun all accept it — and it writes "@properui/icons": "npm:@untitledui/icons@^0.0.22" into your package.json. npx @properui/cli@latest add runs exactly this install for you whenever a component needs icons, and @properui/ui already declares it, so if you installed the component library you have it. Installing it directly only matters if you're using icons standalone, outside of any Proper UI component.
Using Proper UI Icons
Main module import (recommended)
Import icons by name from the package root. With Next.js or Vite, the bundler tree-shakes unused icons automatically — you only ship the ones you import, regardless of import style.
import { ArrowRight, Home01, SearchLg } from "@properui/icons";
<Home01 className="text-fg-quaternary size-5" aria-hidden="true" />;
Individual module import
Older bundlers without reliable tree-shaking can import a single icon from its own module path instead, to guarantee only that icon's code is included:
import ArrowRight from "@properui/icons/ArrowRight";
Reach for this only if you've measured a bundle-size problem — with Next.js or Vite you don't need it, and mixing import styles in one project adds noise for no benefit.
Styling Proper UI Icons
Icons are plain SVG React components: each one accepts className and the rest of SVGProps<SVGSVGElement>, so they take the same className-driven styling as any other element — text-color utilities for stroke color (icons default to stroke="currentColor"), size utilities for dimensions, and the strokeWidth prop for line thickness.
<ArrowRight className="text-brand-600 size-5" strokeWidth={2} aria-hidden="true" />
| Concern | How | Values used across the library |
|---|---|---|
| Size | size-* utility | size-4 (16px, Button's xs size), size-5 (20px, Button's default and most other contexts) |
| Color | text-fg-* token | text-fg-quaternary (default, decorative), text-fg-primary (emphasized), text-fg-brand-primary (brand accent) |
| Line weight | strokeWidth prop | 2 by default; Button at xs size uses 2.25 for better legibility at small sizes |
| Accessibility | aria-hidden / parent aria-label | decorative icons get aria-hidden="true"; icon-only buttons put the label on the button, not the icon |
Passing an icon into a component's iconLeading / iconTrailing prop as a component reference — <Button iconLeading={ArrowRight}> — lets the component apply sizing and the data-icon attribute for you. Internally, Button (see packages/ui/src/components/base/buttons/button.tsx) checks which form you passed with an isReactComponent helper (packages/ui/src/utils/is-react-component.ts, which detects function, forward-ref, and class components) and, when it's a component reference, renders it itself as <IconLeading data-icon="leading" className={styles.common.icon} />. That's why every spec in this library prefers the component-reference form over passing a rendered element — it's the form Button was written to size and label for you.
// preferred — component reference: Button applies size and data-icon for you
<Button iconLeading={ArrowRight}>Continue</Button>
// also supported — a rendered element: Button renders it as-is, so you own data-icon and sizing
<Button iconLeading={<ArrowRight data-icon className="size-4" />}>Continue</Button>
The data-icon attribute isn't just bookkeeping — Button's styles target it directly (for example *:data-icon:text-white/60 on the primary color, or hiding every non-loading icon during isLoading with a [&>*:not([data-icon=loading])] selector), so a rendered element that omits data-icon won't pick up those states.
Standalone, outside of any component, add aria-hidden="true" to decorative icons and give the surrounding control an aria-label when the icon is the only content:
<button aria-label="Close" className="outline-focus-ring focus-visible:outline-2">
<X className="text-fg-quaternary size-5" aria-hidden="true" />
</button>