Button
Trigger an action or event — submitting a form, opening a dialog, confirming a
change. Button is for actions that mutate state; for navigation that
changes the URL, use ButtonLink.
Visual style is two axes: variant is the surface (filled / outlined /
ghost / effect), and color is the semantic ink (default /
destructive).
Import
import { Button, ButtonLink } from "@photon-ai/pho-ui/components/button";
Sizes
Three sizes — sm, md, lg. The default is md.
<Button size="sm">Upload</Button>
<Button size="md">Upload</Button>
<Button size="lg">Upload</Button>
Variants
variant is the surface axis:
effect— default; marketing pill with a same-hue 1.5px outer rim and an inset light/shadow sheen that springs up on hover and presses back in while held (always rounded;shapeignored)filled— solid contrast fill without sheenoutlined— hairline ring, supporting actionghost— borderless, quiet / inline action
Every variant shares the same press compression (scale 0.97 while held).
Only effect also springs its sheen.
<Button>Get started</Button>
<Button variant="filled">Upload</Button>
<Button variant="outlined">Upload</Button>
<Button variant="ghost">Upload</Button>
Colors
color is the semantic axis. Closed set for now: default and
destructive.
Pair surface × color deliberately:
| Use | API |
|---|---|
| Dialog / alert confirm | color="destructive" (default effect) |
| Danger-zone trigger in a list | variant="ghost" color="destructive" |
| Cancel / dismiss in a footer | variant="ghost" |
| Supporting / OAuth | variant="outlined" |
| Solid fill without sheen | variant="filled" |
{/* default color — effect is the default surface */}
<Button>Get started</Button>
<Button variant="filled">Upload</Button>
<Button variant="outlined">Upload</Button>
<Button variant="ghost">Upload</Button>
{/* destructive */}
<Button color="destructive">Delete</Button>
<Button variant="filled" color="destructive">Delete</Button>
<Button variant="outlined" color="destructive">Delete</Button>
<Button variant="ghost" color="destructive">Delete</Button>
Shapes
shape is rounded (the default pill — every button on photon.codes),
square for dense app chrome, or circle for round icon buttons. Icon-only
buttons must set svgOnly and an aria-label that names the action.
square uses rounded-base (12px) at every size; lg also adds squircle
smoothing — the same corner treatment as the lg Input, so 44px buttons and
44px fields share a corner language.
import { IconArrowUp } from "@tabler/icons-react";
{/* filled — effect ignores shape */}
<Button variant="filled" svgOnly aria-label="Upload" shape="square">
<IconArrowUp />
</Button>
<Button variant="filled" svgOnly aria-label="Upload" shape="circle">
<IconArrowUp />
</Button>
Prefix and suffix
prefix and suffix are generic slots placed before and after the label. Pass
an icon component (IconPlus) or an element (<IconPlus />).
A side that carries an icon tucks its padding in by the golden ratio
(padding ÷ 1.618 — 14→8px, 16→10px, 20→12px across sm/md/lg): an
icon’s rounded form brings its own whitespace, so full text-edge padding would
read as a hole. The spinner shown by loading counts as an icon too.
import { IconArrowLeft, IconArrowRight, IconPlus } from "@tabler/icons-react";
<Button prefix={IconArrowLeft}>Back</Button>
<Button suffix={IconArrowRight}>Continue</Button>
<Button variant="outlined" prefix={IconPlus} suffix={IconArrowRight}>
New project
</Button>
Loading
Pass loading instead of swapping in your own spinner — the button stays
readable, announces its busy state (aria-busy), and blocks interaction.
<Button loading>Saving</Button>
Disabled
<Button disabled>Upload</Button>
When an effect pill leaves disabled (or loading), it plays a one-shot
wake glint: the inset sheen starts from its pressed-in position, lifts
past the hover line, and settles at rest — the pill’s own hover/press gesture
replayed once to say “ready”. Shadow-only (no movement), skipped under
reduced motion.
<Button disabled={!formValid}>Confirm</Button>
Disabled variants
<Button disabled variant="filled">Filled</Button>
<Button disabled variant="outlined">Outlined</Button>
<Button disabled variant="ghost">Ghost</Button>
<Button disabled variant="filled" color="destructive">Destructive</Button>
Link
Use ButtonLink for navigation. It shares every visual prop with Button and
adds href, rendering the host router’s link through LinkProvider.
import { IconArrowRight } from "@tabler/icons-react";
<ButtonLink href="/docs" variant="outlined" suffix={IconArrowRight}>
View docs
</ButtonLink>;
Icon reveal
iconReveal is a separate boolean (default false). When true, prefix /
suffix icons are collapsed and invisible at rest, then spring in on hover —
independent of variant, so it works on filled, outlined, ghost, and effect.
import { IconArrowRight } from "@tabler/icons-react";
<Button iconReveal suffix={IconArrowRight}>Continue</Button>
<Button variant="outlined" iconReveal suffix={IconArrowRight}>
Continue
</Button>
<Button iconReveal size="lg" suffix={IconArrowRight}>
Book a demo
</Button>
Props
- variant —
effect·filled·outlined·ghost(defaulteffect) - color —
default·destructive(defaultdefault) - size —
sm·md·lg(defaultmd) - shape —
rounded·square·circle(defaultrounded; ignored whenvariant="effect") - iconReveal —
boolean(defaultfalse) — spring prefix/suffix icons in on hover (any variant) - prefix / suffix — a slot before / after the label (icon component or element)
- svgOnly — render an icon-only button; requires
aria-label - loading — show a spinner and block interaction
- disabled — native disabled state
- href (
ButtonLinkonly) — navigate viaLinkProvider
Accessibility
- Semantics —
Buttonrenders a native<button type="button">;ButtonLinkrenders an<a>throughLinkProvider. - Disabled — a disabled
Buttonuses the nativedisabledattribute. A disabledButtonLinksetsaria-disabledand turns off pointer events. - Icon-only —
svgOnlyrequires anaria-labelnaming the action; in development a missing label logs a console warning. - Loading —
loadingsetsaria-busyand blocks pointer events while keeping the label in place. - Focus — focus shows a 2px outline on
:focus-visibleonly; destructive surfaces use an error-toned outline. - Motion — press compression (every variant), the effect sheen spring,
and the wake glint are decorative and carry no ARIA state; none run while
disabledorloading, and the wake glint respectsprefers-reduced-motion.
Keyboard:
- Enter / Space — activate a
Button - Enter — follow a
ButtonLink
Best practices
- Use
Buttonfor actions that mutate state; useButtonLinkfor navigation. - Pick surface (
variant) for emphasis, then color for intent. Dialog confirms are defaulteffect+destructive; danger-zone list triggers areghost+destructive— never both a header X and a footer confirm in the same dialog (actionsowns dismiss). - Prefer bare
<Button>over the deprecatedEffectButtonalias; usevariant="filled"when you want solid without sheen. - Pass
loadingrather than swapping in a spinner. - Disable only when the action is impossible right now; pair with a tooltip that explains why.
- Title-case the label and name what happens:
Delete Project, notOK. - Icon-only buttons require both
svgOnlyandaria-label.