Foundations

Icons

Consistent icon sizes, control spacing, and accessible usage across Migoy.

Direction

Use Lucide icons from lucide-react. Choose familiar symbols that reinforce visible text or identify a clearly named action. Use a stroke width of 1.75 for 14px and 16px icons, and 2 for icons above 16px. Keep one icon family throughout the interface.

Migoy defines the scale below for its 24/32/44px controls. The components apply it automatically. This is a Migoy convention built on shadcn, rather than a universal shadcn icon-size rule.

Control size scale

ControlHeightIcon beside textIcon-only Button
Small24px14px14px
Default32px16px16px
Large44px16px20px

Large controls keep 16px icons beside text because the text remains 14px. Large icon-only buttons use a 20px symbol for more presence. The icon's visible size is separate from its clickable area.

Small · 24px control

Inline 14px · icon-only 14px

Default · 32px control

Inline 16px · icon-only 16px

Large · 44px control

Inline 16px · icon-only 20px

These examples render the actual Button and Spinner components. The saving controls are static loading specimens. Their icons have no per-example size classes.

import { Plus, Search } from 'lucide-react';
import { Button } from '@/components/ui/button';

<Button size="sm">
  <Plus data-icon="inline-start" aria-hidden="true" />
  Create item
</Button>

<Button size="icon-lg" variant="outline" aria-label="Search projects">
  <Search aria-hidden="true" />
</Button>

Let the component choose the icon dimensions. Use data-icon="inline-start" or data-icon="inline-end" for icons beside Button text. Do not add routine size-*, width, or height overrides to every icon.

Input Group icons and spacing

Input Group uses 14px icons in small groups and 16px in default and large groups. Embedded button icons and spinners follow the group's size, including in a large group; the 20px exception belongs to a standalone large icon-only Button.

Text addons keep 12px outer padding at the default size.

RelationshipSpacing
Outer edge to a decorative inline icon8px, on either side
Inline addon to editing text8px
Text-only outer edge, small / default / large8px / 12px / 16px
Label to control; control to description or error4px

Handle each edge independently. A leading icon uses 8px while a plain trailing text edge keeps the size's standard padding. The 8px icon inset applies to decorative icons, not embedded buttons: buttons retain their own padding and visible focus treatment.

Put the editing control first in the DOM, followed by its addons. Use logical inline-start and inline-end alignment rather than manually positioning left and right icons.

import { Search } from 'lucide-react';
import { InputGroup, InputGroupInput, InputGroupAddon } from '@/components/ui/input-group';

<InputGroup>
  <InputGroupInput id="project-search" aria-label="Search projects" />
  <InputGroupAddon>
    <Search aria-hidden="true" />
  </InputGroupAddon>
</InputGroup>

Loading indicators

Use the shared Spinner when an action is pending. Inside Button or Input Group it inherits the control's icon size; standalone Spinner defaults to 16px. Keep a visible progress label and use aria-busy on the relevant control. Spinner respects reduced-motion preferences.

import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';

<Button size="sm" disabled aria-busy="true">
  <Spinner data-icon="inline-start" aria-hidden="true" />
  Saving changes
</Button>

When visible text already describes progress, hide the decorative spinner from assistive technology. For dynamically changing progress, provide a concise status announcement. Avoid announcing both the same text and a separate spinner label.

Standalone icons

Outside a component that owns sizing, set the size explicitly. Use 16px as the general default and 20px for prominent navigation; 14px can accompany compact metadata. Preserve the same visual style across the interface.

14px · size-3.5
16px · size-4
20px · size-5
import { Search } from 'lucide-react';

<Search className="size-4" aria-hidden="true" />

This explicit sizing is for standalone icons. Inside Migoy controls, use the component's size variant. Keep custom exceptions deliberate and check them in context.

Accessibility

  • Hide decorative icons with aria-hidden="true" when nearby text already communicates their meaning.
  • Give every icon-only action an accessible name, such as aria-label="Search projects". An icon's component name is not an accessible label.
  • Tooltips can supplement an action's name; keep essential instructions visible.
  • Do not rely on color or an unfamiliar symbol alone to explain status, errors, or destructive actions.
  • Keep icons out of the Tab order. The containing button or input receives focus.
  • Prefer 44px controls for touch. A 14px or 20px glyph does not determine the size of its interactive target.

Implementation

Button and Input Group own their icon scale in their source variants. Spinner inherits it through the local --control-icon-size CSS property, with a 16px standalone fallback. Controls also own stroke width through --control-icon-stroke: 1.75 at 14–16px and 2 for the 20px large icon-only button. Standalone icons should set strokeWidth explicitly to match their size. This is a component implementation detail, not a global theme setting that products must configure.

Import Lucide icons directly by name. If the consuming app does not already have the package, install it with pnpm add lucide-react. Installing Button alone does not install an icon library. Spinner declares its own Lucide dependency.

Use the component pages for installation and full examples: Button, Input Group, and Tooltip.

The documentation interface uses the same stroke scale for its 14px and 16px navigation and utility icons. Larger icons retain stroke width 2. Live component specimens keep their component-owned styling.

On this page