Button
Buttons trigger an action — submitting a form, opening a dialog, confirming a choice. Built on Base UI, with an emphasis level for every job and full keyboard and screen-reader support.
Variants
Seven emphasis levels. Use one neutral-white primary per view, reach for accent when a second action needs weight, and let secondary, outline, and ghost carry the rest; danger is reserved for destructive actions, and link sits inline with text.
import { Button } from "@stridge/noctis";
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary">Primary</Button>
<Button variant="accent">Accent</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
<Button variant="link">Link</Button>
</div>
);
}
Sizes
Four heights — extra-small through large — sharing one type and spacing rhythm.
import { Button } from "@stridge/noctis";
export default function ButtonSizes() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
);
}
With icons
Pair a leading or trailing glyph with the label. Icons are decorative; the label names the action.
"use client";
import { Button } from "@stridge/noctis";
import { ArrowRight, Download } from "lucide-react";
export default function ButtonWithIcons() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="secondary" startIcon={Download}>
Download
</Button>
<Button variant="secondary" endIcon={ArrowRight}>
Continue
</Button>
</div>
);
}
Icon only
Square the button for a lone glyph. Always pass an aria-label — without visible text, assistive technology needs it to announce the action.
"use client";
import { Button } from "@stridge/noctis";
import { Plus, Trash2 } from "lucide-react";
export default function ButtonIconOnly() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button iconOnly aria-label="Add" startIcon={Plus} />
<Button variant="secondary" iconOnly aria-label="Add" startIcon={Plus} />
<Button variant="ghost" iconOnly aria-label="Delete" startIcon={Trash2} />
</div>
);
}
Loading
A loading button shows a centered spinner, blocks interaction, and keeps its size so the layout never jumps.
import { Button } from "@stridge/noctis";
export default function ButtonLoading() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button loading>Saving</Button>
<Button variant="secondary" loading>
Loading
</Button>
</div>
);
}
Disabled
A disabled button dims to the disabled opacity and ignores interaction.
import { Button } from "@stridge/noctis";
export default function ButtonDisabled() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button disabled>Primary</Button>
<Button variant="secondary" disabled>
Secondary
</Button>
</div>
);
}
Full width
Stretch the button to fill its container — common in narrow forms and on mobile.
import { Button } from "@stridge/noctis";
export default function ButtonFullWidth() {
return (
<div className="w-full max-w-sm">
<Button fullWidth>Continue</Button>
</div>
);
}
Link as button
For navigation, style a link with buttonVariants so it stays a real anchor while wearing the button's look; use the Button itself for in-place actions.
import { buttonProps } from "@stridge/noctis/props";
export default function ButtonAsLink() {
return (
<div className="flex flex-wrap items-center gap-3">
<a href="#button" {...buttonProps({ variant: "primary" })}>
Go to dashboard
</a>
<a href="https://stridge.com" {...buttonProps({ variant: "secondary" })}>
Stridge.com
</a>
</div>
);
}
Scoped radius
Noctis is pill by default — controls go fully round — but RadiusScope is the escape hatch: wrap a region and every primitive inside re-rounds — sharp buttons for a dense dashboard, a subtler radius for a settings panel — without touching the app-wide setting. Surfaces stay capped, so cards never pill.
import { Button, RadiusScope } from "@stridge/noctis";
export default function ButtonScopedRadius() {
return (
<RadiusScope radius="pill">
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary">Get started</Button>
<Button variant="secondary">Learn more</Button>
</div>
</RadiusScope>
);
}
On surfaces
The same control re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.
Design tokens
Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every button in that region retunes — e.g. .marketing { --noctis-button-border-radius: 9999px; } pills every button beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.
API reference
Generated from the component's types — every prop, type, default, and description comes straight from the source. Button extends the Base UI Button props, so nativeButton and the native button attributes pass straight through; expand a row for the full type and description.