Button
The primary way to trigger an action. Lifts on hover and sinks into its shadow on press. Also available as a class helper for links.
stableButton.tsx
Examples
Variants
button/variants.tsx
import { Button } from "@streamui/react";
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center justify-center gap-4">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="pink">Pink</Button>
<Button variant="blue">Blue</Button>
</div>
);
}Sizes
Icon buttons need an aria-label.
button/sizes.tsx
import { Plus } from "lucide-react";
import { Button } from "@streamui/react";
export default function ButtonSizes() {
return (
<div className="flex flex-wrap items-center justify-center gap-4">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button size="icon" aria-label="Add item">
<Plus aria-hidden />
</Button>
</div>
);
}With icons
Icons size themselves; mark them aria-hidden.
button/with-icons.tsx
import { ArrowRight, Download, Trash } from "lucide-react";
import { Button } from "@streamui/react";
export default function ButtonWithIcons() {
return (
<div className="flex flex-wrap items-center justify-center gap-4">
<Button variant="secondary">
<Download aria-hidden /> Export CSV
</Button>
<Button>
Continue <ArrowRight aria-hidden />
</Button>
<Button variant="pink" disabled>
<Trash aria-hidden /> Delete
</Button>
</div>
);
}As a link
Navigation should be a link, not a button. buttonStyles() gives a link the same look.
button/as-link.tsx
import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { buttonStyles } from "@streamui/react";
export default function ButtonAsLink() {
return (
<Link href="/components" className={buttonStyles({ variant: "blue" })}>
Browse components <ArrowUpRight aria-hidden />
</Link>
);
}Installation
bun add @streamui/reactusage.tsx
import { Button } from "@streamui/react";
<Button variant="primary" size="md">Save changes</Button>API
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "primary" | "secondary" | "blue" | "pink" | "primary" | Fill color. |
| size | "sm" | "md" | "lg" | "icon" | "md" | Height and padding. icon is a square for a single glyph. |
| type | "button" | "submit" | "reset" | "button" | Defaults to button so it never submits a form by accident. |
| className | string | — | Merged last, so it can override any default class. |
| ...props | ComponentProps<"button"> | — | Every native button attribute. |
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonVariant | "primary" | Same as Button. |
| size | ButtonSize | "md" | Same as Button. |
| className | string | — | Extra classes merged last. |
Accessibility
- Renders a native <button>, so Enter and Space work and it is announced as a button.
- Focus shows a 3px blue ring; hover and press are motion only and are removed under reduced motion.
- Disabled buttons drop out of pointer interaction and render at 50% opacity.
Source
packages/react/src/Button.tsx
import type { ComponentProps } from "react";
import { cn } from "./cn";
export type ButtonVariant = "primary" | "secondary" | "blue" | "pink";
export type ButtonSize = "sm" | "md" | "lg" | "icon";
const variants: Record<ButtonVariant, string> = {
primary: "bg-lime text-on-bright",
secondary: "bg-paper-raised text-ink",
blue: "bg-blue text-on-bright",
pink: "bg-pink text-on-bright",
};
const sizes: Record<ButtonSize, string> = {
sm: "h-9 px-3 text-sm",
md: "h-11 px-5 text-sm",
lg: "h-13 px-6 text-base",
icon: "size-11",
};
/**
* Class string for the brutal button look. Use it on links that should look
* like buttons: `<a className={buttonStyles({ variant: "blue" })}>`.
* Hover lifts the button toward the light; press pushes it into its shadow.
*/
export function buttonStyles({
variant = "primary",
size = "md",
className,
}: { variant?: ButtonVariant; size?: ButtonSize; className?: string } = {}) {
return cn(
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 border-2 border-ink font-semibold whitespace-nowrap shadow-brutal select-none",
"transition-[translate,box-shadow,background-color] duration-150 ease-out motion-reduce:transition-none",
"hover:-translate-x-0.5 hover:-translate-y-0.5 hover:shadow-brutal-lg",
"active:translate-x-1 active:translate-y-1 active:shadow-none",
"disabled:pointer-events-none disabled:opacity-50",
"[&_svg]:size-4.5 [&_svg]:shrink-0",
variants[variant],
sizes[size],
className,
);
}
export type ButtonProps = ComponentProps<"button"> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
export function Button({
variant,
size,
className,
type = "button",
...props
}: ButtonProps) {
return (
<button
type={type}
className={buttonStyles({ variant, size, className })}
{...props}
/>
);
}