Button

Starts an action, or takes the reader somewhere else. Five types, three tones, four sizes and two radii, the same as the Figma set, with a loading state.

Installation

bash
npx @smicolon/fasla-ui@latest add button

Run init once per project before adding components. See Installation.

Preview

Variants

variant sets the type. default, secondary and destructive are filled. outline has a border on the inside, ghost fills only when you hover it, and soft lays a light tint over the card surface. link is text, underlined in every state.

Tones

tone picks the colour within a type: primary, secondary or destructive. Each variant has its own default, so variant="outline" on its own is the neutral outline, and adding tone="destructive" makes it the destructive one.

default
soft
outline
ghost
link

Sizes

Four sizes: xs is 26px tall, sm 32px, md 36px and lg 40px. Icons are 16px at every size. For a button with only an icon, use the icon sizes on the Icon Button page.

Radius

radius="standard" rounds the corners by 8px, and radius="rounded" makes a pill. The focus ring follows the shape.

States

Hover, pressed and keyboard focus are real CSS states, not props: hover a button, press it, or tab to it. Filled buttons fade when you hover and press them. Soft, outline and ghost buttons deepen their tint instead, and a link keeps its underline and fades. Disabled dims every variant to half opacity.

Enabled
Disabled

Link or ghost

Both are quiet, but they mean different things. Use ghost for an action on the same page, such as closing a panel, editing a row or opening a menu. Use link to go somewhere else, such as Forgot password?, Learn more or View all, and render it as a real link with asChild.

An action on this page

With icons

Put an icon before or after the label, as a child. The button sizes it to 16px and gives it the label's colour. Icons follow the reading direction; flip one that points, such as an arrow, with rtl:-scale-x-100.

Loading

loading shows a spinner, disables the button and announces loadingLabel to screen readers. With asChild the child is only marked busy (aria-busy) and disabled, so show your own progress inside it.

Accessibility

  • Renders a native button, so Enter and Space press it. Set type="submit" or type="button" as the form needs.
  • Keyboard focus draws a 2px ring, 2px outside the button, in the ring colour.
  • With asChild, the button styles its only child, such as a link. A disabled child gets aria-disabled instead of disabled.
  • While it is loading, the button is disabled and announces loadingLabel as a status.
  • Direction is inherited from dir, never a prop. Icons and label swap ends in right-to-left.

Props

PropTypeDefaultDescription
tone"primary" | "secondary" | "destructive"—The colour within the type. Each variant has its own default.
variant"default" | "outline" | "ghost" | "soft" | "link""default"The type: filled, outline, ghost, soft or link.
size"xs" | "sm" | "md" | "lg" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg""md"The height. The icon sizes are for icon buttons.
radius"standard" | "rounded""standard"The corners: slightly rounded, or a pill.
loadingbooleanfalseShows a spinner and disables the button.
loadingLabelstring"Loading"Screen-reader text while loading.
disabledbooleanfalseDisables the button.
asChildbooleanfalseStyles its only child, such as a link, instead of rendering a button.
onClick(event) => void—Called when the button is pressed.
classNamestring—Extra classes for the component.

Kept for shadcn compatibility: secondary and destructive still work as variant values (Solid in those tones), and default and icon as size values (the same as md and icon-md).

Usage

Basic button

tsx
import { Button } from "@/components/ui/button"

export function SaveButton() {
  return <Button type="submit">Save changes</Button>
}

Outline in the destructive tone

tsx
import { Button } from "@/components/ui/button"

export function DeleteAccountButton() {
  return (
    <Button variant="outline" tone="destructive">
      Delete account
    </Button>
  )
}

A link to another page

tsx
import Link from "next/link"
import { Button } from "@/components/ui/button"

export function ForgotPasswordLink() {
  return (
    <Button variant="link" asChild>
      <Link href="/forgot-password">Forgot password?</Link>
    </Button>
  )
}

With an icon

tsx
import { UploadIcon } from "lucide-react"
import { Button } from "@/components/ui/button"

export function UploadButton() {
  return (
    <Button>
      <UploadIcon aria-hidden="true" strokeWidth={1.5} />
      Upload
    </Button>
  )
}

Loading

tsx
import { Button } from "@/components/ui/button"

export function SavingButton() {
  return (
    <Button loading loadingLabel="Saving">
      Saving…
    </Button>
  )
}