stream/ui
Browse the library

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/react
usage.tsx
import { Button } from "@streamui/react";

<Button variant="primary" size="md">Save changes</Button>

API

Button
PropTypeDefaultDescription
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.
classNamestring—Merged last, so it can override any default class.
...propsComponentProps<"button">—Every native button attribute.
buttonStyles(options)
PropTypeDefaultDescription
variantButtonVariant"primary"Same as Button.
sizeButtonSize"md"Same as Button.
classNamestring—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}
    />
  );
}