Propel

Pill

A pill-shaped button that holds a label with optional inline-start and inline-end icons.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { PillButton } from "@makeplane/propel/components/pill";
import { Tag, X } from "lucide-react";

export default function BasicDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <PillButton size="sm" startIcon={<Icon icon={Tag} />} label="Design" />
      <PillButton size="sm" endIcon={<Icon icon={X} />} label="Dismissible" />
    </div>
  );
}

Installation

import { Icon } from "@makeplane/propel/components/icon";
import { PillButton } from "@makeplane/propel/components/pill";

Usage

import { Icon } from "@makeplane/propel/components/icon";
import { PillButton } from "@makeplane/propel/components/pill";
import { Tag, X } from "lucide-react";

export default function BasicDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <PillButton size="sm" startIcon={<Icon icon={Tag} />} label="Design" />
      <PillButton size="sm" endIcon={<Icon icon={X} />} label="Dismissible" />
    </div>
  );
}

Examples

Sizes

size runs xs 20 / sm 24 / md 28 px — the same floor as Button, stopping one rung below its top, because a pill is inline chrome rather than a primary action. Required on both PillButton and IconPill. See Sizing for the full ladder.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { PillButton, type PillSize } from "@makeplane/propel/components/pill";
import { Tag } from "lucide-react";

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

export default function SizesDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {SIZES.map((size) => (
        <PillButton key={size} size={size} startIcon={<Icon icon={Tag} />} label={size} />
      ))}
    </div>
  );
}

Variants

variant selects the fill treatment: outline (the default, ≈ Button secondary) or soft (≈ Button tertiary).

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { PillButton } from "@makeplane/propel/components/pill";
import { Tag } from "lucide-react";

export default function VariantsDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <PillButton size="sm" variant="outline" startIcon={<Icon icon={Tag} />} label="Outline" />
      <PillButton size="sm" variant="soft" startIcon={<Icon icon={Tag} />} label="Soft" />
    </div>
  );
}

States

disabled and loading both drop the pill to a transparent fill with a dimmed label; loading also hides the icons, shows a spinner after the label, and blocks clicks while staying focusable.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { PillButton } from "@makeplane/propel/components/pill";
import { Tag } from "lucide-react";

export default function StatesDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <PillButton size="sm" startIcon={<Icon icon={Tag} />} label="Default" />
      <PillButton size="sm" startIcon={<Icon icon={Tag} />} disabled label="Disabled" />
      <PillButton size="sm" loading label="Loading" />
    </div>
  );
}

Icon only

IconPill is a square, icon-only pill and requires an aria-label for its accessible name.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { IconPill } from "@makeplane/propel/components/pill";
import { Plus, X } from "lucide-react";

export default function IconOnlyDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <IconPill size="xs" aria-label="Remove label" icon={<Icon icon={X} />} />
      <IconPill size="sm" aria-label="Remove label" icon={<Icon icon={X} />} />
      <IconPill size="md" aria-label="Remove label" icon={<Icon icon={X} />} />
      <IconPill size="sm" aria-label="Add label" disabled icon={<Icon icon={Plus} />} />
      <IconPill size="sm" aria-label="Add label" loading icon={<Icon icon={Plus} />} />
    </div>
  );
}

API Reference

PillButton

The ready-made pill button: grafts Base UI's `Button` behavior onto the styled `PillButton` container (behavior outer, the styled button as the render target), with an optional leading node, the `PillLabel`, an optional trailing node, and a trailing spinner while `loading`. `loading` disables the button while keeping it focusable (`aria-busy`).

PropTypeDefaultDescription
size(required)"xs" | "sm" | "md"
label(required)stringVisible pill label.
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.
variant"outline" | "soft"outlineFill treatment. Defaults to `outline` (≈ Button secondary). `soft` ≈ Button tertiary.
startIconReactNodeElement rendered before the label (inline-start), e.g. `<Icon icon={Tag} />`.
endIconReactNodeElement rendered after the label (inline-end), e.g. `<Icon icon={X} />`.
loadingbooleanfalseBusy state: hides start/end icons, shows a spinner after the label, and blocks clicks.

IconPill

The ready-made icon-only pill: grafts Base UI's `Button` behavior onto the square `IconPill` container (behavior outer, the styled button as the render target), filling it with the icon (or a spinner while `loading`). `loading` disables the button while keeping it focusable (`aria-busy`). An `aria-label` is required for the accessible name.

PropTypeDefaultDescription
size(required)"xs" | "sm" | "md"
icon(required)ReactNodeThe icon element, usually `<Icon icon={...} />`.
aria-labelstringDefines a string value that labels the current element. Required: icon-only pills have no visible text, so they must be labeled. @see aria-labelledby.
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.
loadingbooleanfalseBusy state: swaps the icon for a spinner and blocks clicks.