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
| Control | Height | Icon beside text | Icon-only Button |
|---|---|---|---|
| Small | 24px | 14px | 14px |
| Default | 32px | 16px | 16px |
| Large | 44px | 16px | 20px |
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.
| Relationship | Spacing |
|---|---|
| Outer edge to a decorative inline icon | 8px, on either side |
| Inline addon to editing text | 8px |
| Text-only outer edge, small / default / large | 8px / 12px / 16px |
| Label to control; control to description or error | 4px |
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.
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.