Propel

Button

Triggers an action or event, such as submitting a form or opening a dialog.

Show code
import { Button } from "@makeplane/propel/components/button";

export default function BasicDemo() {
  return <Button label="Button" variant="primary" size="sm" stretch="auto" />;
}

Installation

import { Button } from "@makeplane/propel/components/button";

Usage

import { Button } from "@makeplane/propel/components/button";

export default function BasicDemo() {
  return <Button label="Button" variant="primary" size="sm" stretch="auto" />;
}

Examples

With an icon

Pass icon and pick its side with iconPosition (start by default).

Show code
import { Button } from "@makeplane/propel/components/button";
import { ArrowRight, Plus } from "lucide-react";

export default function WithIconDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button label="New" variant="primary" size="sm" stretch="auto" icon={<Plus />} />
      <Button
        label="Continue"
        variant="secondary"
        size="sm"
        stretch="auto"
        icon={<ArrowRight />}
        iconPosition="end"
      />
    </div>
  );
}

Variants

Variant sets the visual weight and palette: primary, secondary, tertiary, and ghost, plus danger and danger-outline for destructive actions.

Show code
import { Button } from "@makeplane/propel/components/button";

export default function VariantsDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button label="Primary" variant="primary" size="sm" stretch="auto" />
      <Button label="Secondary" variant="secondary" size="sm" stretch="auto" />
      <Button label="Tertiary" variant="tertiary" size="sm" stretch="auto" />
      <Button label="Ghost" variant="ghost" size="sm" stretch="auto" />
      <Button label="Danger" variant="danger" size="sm" stretch="auto" />
      <Button label="Danger outline" variant="danger-outline" size="sm" stretch="auto" />
    </div>
  );
}

Sizes

size sets the height and everything that follows from it — text, glyph, padding, and radius (xs 20 / sm 24 / md 28 / lg 32 px). There is no default: every call site picks one. xs is inline chrome only: it ships a synthesized 24px pointer target, which is why the grouped families have no xs rung at all — ButtonGroup runs smmd and SplitButton smlg, since their segments touch and adjacent targets would intersect. See Sizing for the full ladder.

Show code
import { Button, type ButtonSize } from "@makeplane/propel/components/button";

const SIZES: ButtonSize[] = ["xs", "sm", "md", "lg"];

export default function SizesDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {SIZES.map((size) => (
        // The label carries a word as well as the rung: with a two-character label every button
        // hits its min-width floor (internal/button-geometry.ts) and the padding ramp stops
        // being visible.
        <Button key={size} label={`Button ${size}`} variant="primary" size={size} stretch="auto" />
      ))}
    </div>
  );
}

Loading

The loading state shows a spinner in the icon slot, sets aria-busy, and blocks interaction while staying focusable.

Show code
import { Button } from "@makeplane/propel/components/button";

export default function LoadingDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button label="Saving" variant="primary" size="sm" stretch="auto" loading />
      <Button label="Inviting member" variant="secondary" size="sm" stretch="auto" loading />
      <Button label="Please wait" variant="tertiary" size="sm" stretch="auto" loading />
    </div>
  );
}

Full width

stretch="full" makes the button fill its container, e.g. a form row or a mobile CTA.

Show code
import { Button } from "@makeplane/propel/components/button";

export default function FullWidthDemo() {
  return (
    <div className="flex w-64 flex-col gap-2">
      <Button label="Full-width" variant="primary" size="sm" stretch="full" />
      <Button label="Full-width outline" variant="secondary" size="sm" stretch="full" />
    </div>
  );
}

Disabled

disabled is the hard, non-focusable disabled state; unlike loading, it removes the button from the tab order.

Show code
import { Button } from "@makeplane/propel/components/button";

export default function DisabledDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button label="Save changes" variant="primary" size="sm" stretch="auto" disabled />
      <Button label="Delete workspace" variant="danger-outline" size="sm" stretch="auto" disabled />
    </div>
  );
}

render={<a href=… />} swaps the underlying element and nativeButton={false} applies Base UI’s non-native button semantics: the control is announced as a button while the <a> keeps native navigation.

Show code
import { Button } from "@makeplane/propel/components/button";

export default function AsLinkDemo() {
  return (
    <Button
      label="Open reports"
      variant="secondary"
      size="sm"
      stretch="auto"
      nativeButton={false}
      render={<a href="#reports" />}
    />
  );
}

API Reference

Button

The ready-made `Button`: grafts Base UI's `Button` behavior onto the styled `Button` element and lays out an optional `icon` beside the label (`iconPosition`), swapping it for the `loading` spinner in the same slot. Content — the label, icon, and `loading` state — is not a variant.

PropTypeDefaultDescription
variant(required)"primary" | "secondary" | "tertiary" | "ghost" | "danger" | "danger-outline"
size(required)"xs" | "sm" | "md" | "lg"
stretch(required)"auto" | "full"
label(required)stringVisible button label.
disabledbooleanHard, non-focusable native disabled state (`loading` stays focusable instead).
type"button" | "submit" | "reset""button"The button's form behavior.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
nativeButtonbooleanSet `false` when `render` swaps the underlying tag away from `<button>` (e.g. a `<div>` or an `<a>`): Base UI then adds `role="button"`, tab focus, and Enter/Space activation.
iconReactNodeIcon rendered beside the label (inline-start by default), e.g. `<Icon icon={Plus} />`.
iconPosition"start" | "end""start"Which side of the label the icon sits on. The `loading` spinner takes the same slot.
loadingbooleanShows a spinner in the icon slot, sets `aria-busy`, and makes the button non-interactive.