# Shadcn icons in React apps: choose one set and ship a consistent interface

> A practical way to choose shadcn icons for React: verify imports, visual fit, labels, and app ownership.
> By Dave · 2026-10-12
> Source: https://otf-kit.dev/blog/shadcn-icons-react-consistent-ui

If you searched for **shadcn icons**, you are probably looking for an icon set that fits a React app built with shadcn/ui. The useful first step is to separate the component system from the icon package: shadcn/ui’s manual setup includes `lucide-react` and an `iconLibrary` setting, so the icon family is a project choice. In OTF’s shared React UI package, the dependency is also `lucide-react`. Pick a set that fits the interface you are shipping, then keep its visual rules consistent across the app.

## What “shadcn icons” means in practice

Shadcn/ui does not require every app to use one proprietary icon pack. Its official setup describes dependencies and a project-level icon library choice. That distinction matters when you search for `shadcn icons`: the phrase points to icon choices in a shadcn-style project, not to a separate package that supplies every icon for you.

Treat the package as an implementation decision inside your application. A component can render an icon from one package, another component can use a different family, and a page can still look inconsistent even though every SVG works. Your goal is to establish which library the current code uses, decide whether to keep it, and give new UI one clear convention.

## Audit the icon package you already have

Before installing anything, inspect the project’s configuration and imports. Check `components.json` for `iconLibrary`, then inspect the app or workspace `package.json` and search component source for imports such as `from "lucide-react"`. Package managers can help identify why a dependency is present: `pnpm why lucide-react` or `npm ls lucide-react` will show whether it is direct or transitive.

If your own application source imports an icon package, declare that package in the workspace that owns the source. Do not rely on a dependency being hoisted from a component library. That makes the app’s requirements explicit and avoids a build that only works because of one lockfile layout.

In OTF’s current shared React package, `lucide-react` is a declared dependency and the dashboard sidebar imports symbols such as `LayoutDashboard`, `FolderOpen`, and `Settings` from it. If you are already using those components, matching their icon family keeps new navigation and actions visually aligned. The [OTF component catalog](https://otf-kit.dev/components) is the place to inspect the component set before adding another package.

## Compare icons in the interface you are building

Do not choose a library from a list of icon counts alone. A large catalog does not help if its symbols are difficult to distinguish at the sizes your product uses. Pick a small set of representative jobs from your actual interface: open search, show account settings, mark a successful save, indicate an alert, and navigate to a primary section.

Render those candidates in the same header, sidebar, table row, and compact button. Look at shape, stroke weight, fill, and visual density at the sizes the interface needs. A magnifying glass that is clear at 24 pixels can turn into an indistinct spot at 16 pixels; a detailed symbol can lose its internal gaps when it sits beside dense text. Compare in both light and dark themes and next to your real type scale, not on an isolated icon grid.

Then test the meaning, not just the appearance. If two symbols could plausibly mean “edit,” “open,” or “configure,” write the action labels beside them and ask whether a new user would choose the right one. For status, pair an icon with text or another visible signal when color alone would carry the meaning. A symbol that needs a paragraph of explanation is a poor choice for a small control.

![Dex and Nova review a consistent icon style across search, settings, navigation, and status controls.](https://cdn.otf-kit.dev/blog/shadcn-icons-react-consistent-ui/inbody1-ui-consistency-20261012b.png)

## Set shared sizing and color rules

Once you choose a family, establish a few shared rules. Use a small size scale that matches your controls, such as a compact size for dense menus and a larger size for prominent actions. Keep stroke width consistent unless a specific symbol needs an optical adjustment. Use the current text color for ordinary actions, and reserve accent colors for a meaningful state rather than decorating every icon.

The icon should sit on the same visual baseline as its label, with enough space that the two do not read as one shape. Align its container and the clickable control separately: the SVG can be 18 pixels while the button remains large enough to hit comfortably. Avoid one-off margin and color fixes in every call site; put common values in a component or design token so a later adjustment reaches the entire interface.

For example, a search button can keep its accessible name on the button while treating the drawing as decorative:

```tsx
import { Search } from "lucide-react";

export function SearchButton() {
  return (
    <button type="button" aria-label="Search">
      <Search aria-hidden="true" size={18} strokeWidth={1.75} />
    </button>
  );
}
```

The exact values are design choices, not defaults every product should copy. Check the rendered result beside your text and other controls. If a standard icon looks too heavy or too faint at 16 pixels, adjust the shared token or choose a clearer symbol rather than applying unrelated fixes across the app.

## Give every icon-only action an accessible name

An icon is not automatically a label. When a button contains only a symbol, give the button an accessible name that describes its action: “Open navigation,” “Search,” or “Delete report.” When visible text already names the action, keep the icon decorative so assistive technology does not announce the same action twice.

Do not rely on a tooltip as the only name. Tooltips can be useful for sighted users, but they may appear only after a hover or delay. Test keyboard focus and confirm that the control still has a meaningful name when there is no pointer. Also check that a disabled, loading, or destructive state is communicated in text or behavior, not only with a color change in the icon.

![Luna and Byte compare an unlabeled icon-only action with a clearly identified icon action.](https://cdn.otf-kit.dev/blog/shadcn-icons-react-consistent-ui/inbody2-icon-accessibility-20261012b.png)

Keep the accessible name on the interactive element rather than on an SVG that is nested inside a button. That gives the control one clear label and leaves the icon free to be decorative. If a symbol itself conveys information, expose that information deliberately and do not hide it as decoration. The right choice depends on whether the icon is an action cue or the only presentation of a status.

## Keep the set consistent as the product grows

Use one family for the common interface first: navigation, form actions, status, and repeated controls. If a rare feature needs a specialized symbol, make the exception visible in code review and keep its size and color aligned with the rest. Importing two full libraries for a single missing icon adds another visual language and another dependency for the team to maintain.

Avoid writing an opaque string-to-icon layer before you need one. Explicit imports make it easier to find where a symbol is used and review the meaning of a control. If a design system does need a wrapper, keep it narrow: expose the size and styling choices the product actually supports, preserve the icon component’s accessibility props, and do not make callers guess which library a name resolves to.

If your product also has a native app, check that path separately. A matching component name does not guarantee that the web and native packages use the same runtime or icon renderer. Our guide to [shared component names across web and mobile](https://otf-kit.dev/blog/one-component-api) explains why platform implementations need their own verification.

## Check the result before shipping

Run through a small review on the real page where each icon appears:

1. Confirm the app uses the intended package and that imports resolve in a clean install.
2. Check the smallest size at normal zoom and on a narrow viewport.
3. Compare the same symbols in light and dark themes and beside the actual text labels.
4. Use the keyboard to reach every icon-only control and verify its accessible name.
5. Confirm that status and destructive actions remain understandable without color alone.
6. Search for imports from a second icon family and decide whether each one is intentional.

This review catches the common failure: a page where every icon technically renders, but the product feels assembled from unrelated starter kits. The goal is not to maximize the number of symbols. It is to make repeated actions easy to recognize, make controls usable without sight or a mouse, and keep the package choice understandable to the next person who edits the interface.

Start with the [OTF component catalog](https://otf-kit.dev/components) and install the free SDK from its instructions when you want individual components for your own app. If you need a larger starting point with the surrounding app structure, compare the [available kits and templates](https://otf-kit.dev/templates), preview the fit, and move from SDK installation to a kit checkout only when that broader foundation matches the project.

## Sources

- [shadcn/ui manual installation](https://ui.shadcn.com/docs/installation/manual) — dependency setup and the `iconLibrary` configuration field.
