Patterns
Icon Box
A square with a border that holds one icon and shows the kind of an item.
Patterns
A square with a border that holds one icon and shows the kind of an item.
import { IconBox } from 'ferry-ui'
import { FolderKanban, KeyRound, Receipt } from 'lucide-react'
const RESULTS = [
{ icon: <FolderKanban />, name: 'Billing portal', kind: 'Project' },
{ icon: <Receipt />, name: 'INV-2041', kind: 'Invoice' },
{ icon: <KeyRound />, name: 'Analytics export', kind: 'API key' },
]
export default function IconBoxHero() {
return (
<ul className="w-full max-w-sm divide-y rounded-lg border bg-surface-100">
{RESULTS.map((result) => (
<li key={result.name} className="flex items-center gap-3 px-4 py-3">
<IconBox size="sm">{result.icon}</IconBox>
<span className="flex min-w-0 flex-col">
<span className="truncate text-sm text-foreground">{result.name}</span>
<span className="text-[13px] text-foreground-lighter">{result.kind}</span>
</span>
</li>
))}
</ul>
)
}IconBox before a name in a list row, a card, a page title or a tile.icon size.Import the component. It has one part. The child is the icon.
import { IconBox } from 'ferry-ui'
<IconBox>
<Icon />
</IconBox>The size prop sets the size of the box and of the icon. Select the size from the place of the box.
import { IconBox, type IconBoxSize } from 'ferry-ui'
import { Receipt } from 'lucide-react'
const SIZES: IconBoxSize[] = ['xs', 'sm', 'md', 'lg', 'xl']
export default function IconBoxSizes() {
return (
<div className="flex flex-wrap items-end gap-6">
{SIZES.map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<IconBox size={size}>
<Receipt />
</IconBox>
<span className="text-[13px] text-foreground-lighter">{size}</span>
</div>
))}
</div>
)
}| Size | Box | Icon | Place |
|---|---|---|---|
xs | 28px | 14px | Dense rows and compact choice cards. |
sm | 32px | 16px | List rows and strips. |
md (default) | 36px | 18px | Choice cards and card headers. |
lg | 44px | 20px | Next to a page title or a dialog title. |
xl | 56px, then 72px from the md breakpoint | 20px | The tiles of Info Tile. |
The tone prop sets the color. The primary tone marks the current item or the selected item.
import { IconBox, type IconBoxTone } from 'ferry-ui'
import { Webhook } from 'lucide-react'
const TONES: IconBoxTone[] = ['neutral', 'primary', 'success', 'warning', 'destructive', 'info']
export default function IconBoxTones() {
return (
<div className="flex flex-wrap items-end gap-6">
{TONES.map((tone) => (
<div key={tone} className="flex flex-col items-center gap-2">
<IconBox tone={tone}>
<Webhook />
</IconBox>
<span className="text-[13px] text-foreground-lighter">{tone}</span>
</div>
))}
</div>
)
}The success, warning, destructive and info tones show a state. In this list, the color of each box shows the kind of event.
import type { ReactNode } from 'react'
import { IconBox, type IconBoxTone } from 'ferry-ui'
import { AlertTriangle, CreditCard, Receipt, UserPlus } from 'lucide-react'
const EVENTS: { tone: IconBoxTone; icon: ReactNode; title: string; meta: string }[] = [
{ tone: 'success', icon: <Receipt />, title: 'Invoice INV-2041 paid', meta: '2 hours ago' },
{ tone: 'info', icon: <UserPlus />, title: 'Maya Chen is now a member', meta: '5 hours ago' },
{ tone: 'warning', icon: <CreditCard />, title: 'The card expires this month', meta: 'Yesterday' },
{ tone: 'destructive', icon: <AlertTriangle />, title: 'Webhook delivery failed', meta: 'Yesterday' },
]
export default function IconBoxStatus() {
return (
<ul className="flex w-full max-w-sm flex-col gap-4">
{EVENTS.map((event) => (
<li key={event.title} className="flex items-center gap-3">
<IconBox size="sm" tone={event.tone}>
{event.icon}
</IconBox>
<span className="flex min-w-0 flex-col">
<span className="truncate text-[13px] text-foreground">{event.title}</span>
<span className="text-[13px] text-foreground-lighter">{event.meta}</span>
</span>
</li>
))}
</ul>
)
}The elevated prop adds the shadow of a card. It is on by default for the lg and xl sizes. With the neutral tone, it also makes the color of the icon one step stronger.
import { IconBox } from 'ferry-ui'
import { FolderKanban } from 'lucide-react'
export default function IconBoxElevated() {
return (
<div className="flex flex-col gap-5">
<div className="flex items-center gap-3">
<IconBox size="lg">
<FolderKanban />
</IconBox>
<span className="text-2xl font-medium text-foreground">Billing portal</span>
</div>
<div className="flex items-center gap-3 text-[13px] text-foreground-light">
<IconBox>
<FolderKanban />
</IconBox>
<IconBox elevated>
<FolderKanban />
</IconBox>
The md size, flat and with the shadow
</div>
</div>
)
}With no label, the box is decorative. This is correct when the name is next to the box. If the icon is the only sign, set label.
import { IconBox } from 'ferry-ui'
import { Database, KeyRound } from 'lucide-react'
export default function IconBoxLabel() {
return (
<div className="flex flex-col gap-3 text-sm text-foreground">
{/* The name is next to the box: the box is decorative. */}
<div className="flex items-center gap-3">
<IconBox size="sm">
<Database />
</IconBox>
Database
</div>
{/* No type name is next to the box: the label gives it to screen readers. */}
<div className="flex items-center gap-3">
<IconBox size="sm" label="API key">
<KeyRound />
</IconBox>
<span className="font-mono text-[13px]">sk_test_07be</span>
</div>
</div>
)
}The box sets the size of the icon. To use another icon size, add a class such as [&_svg]:size-4 to the box. A size class on the icon has no effect.
import { IconBox } from 'ferry-ui'
import { Receipt } from 'lucide-react'
export default function IconBoxIconSize() {
return (
<>
<IconBox size="xs">
<Receipt />
</IconBox>
<IconBox size="xs" className="[&_svg]:size-4">
<Receipt />
</IconBox>
</>
)
}iconBoxVariants returns the classes of the box. Use it for an element that cannot be an IconBox. If the element is decorative, add aria-hidden.
import { iconBoxVariants } from 'ferry-ui'
import { BellRing } from 'lucide-react'
export default function IconBoxVariants() {
return (
<div className="flex items-center gap-3">
{/* A <div> with the look of the box. It is decorative: hide it from screen readers. */}
<div aria-hidden="true" className={iconBoxVariants({ size: 'lg', tone: 'primary' })}>
<BellRing />
</div>
<span className="flex flex-col">
<span className="text-sm font-medium text-foreground">Alerts are on</span>
<span className="text-[13px] text-foreground-lighter">You get an email for each failed payment.</span>
</span>
</div>
)
}IconBox also accepts each attribute of the <span> element.
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "xs" | md |
Box and icon size, see | ||
tone | "primary" | "destructive" | "warning" | "success" | "info" | "neutral" | neutral |
Colour, see | ||
elevated | boolean | false |
Adds the subtle card shadow (and, for | ||
label | string | - |
Accessible name ("Invoice", "API key"). Without it the box is decorative ( | ||
children | ReactNode | - |
The icon: a lucide icon or any | ||
| Name | Type | Role |
|---|---|---|
iconBoxVariants | ({ size, tone, elevated }) => string | Returns the classes of an IconBox. Each option has the same default as the prop. |