# Pill

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

## Installation

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { IconPill, Pill, PillGroup } from "@makeplane/propel/components/pill";
import { Swatch } from "@makeplane/propel/elements/swatch";
```
## Usage

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Pill } from "@makeplane/propel/components/pill";
import { ReleaseTagOutline, CloseOutline } from "@makeplane/propel/icons";

export default function BasicDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Pill size="sm" startIcon={<Icon icon={ReleaseTagOutline} />} label="Design" />
      <Pill size="sm" endIcon={<Icon icon={CloseOutline} />} 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
`Pill` and `IconPill`, except inside a [`PillGroup`](#a-connected-group), which shares one `size`
with every segment. See [Sizing](/foundations/sizing) for the full ladder.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Pill, type PillSize } from "@makeplane/propel/components/pill";
import { ReleaseTagOutline } from "@makeplane/propel/icons";

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

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

### Variants

`variant` selects the fill treatment: `outline` (the default, ≈ Button secondary), `soft` (≈ Button tertiary) or `ghost` (borderless and transparent at rest — for a trigger whose content is its own box, such as an avatar stack).

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Pill } from "@makeplane/propel/components/pill";
import { ReleaseTagOutline } from "@makeplane/propel/icons";

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

### Placeholder

`placeholder` marks a trigger with no value yet. The chrome stays the same; the label and any glyph that inherits the pill's colour dim to the placeholder colour, and stay dim while the picker is open.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Pill } from "@makeplane/propel/components/pill";
import { CalendarOutline } from "@makeplane/propel/icons";

export default function PlaceholderDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Pill size="xs" placeholder startIcon={<Icon icon={CalendarOutline} />} label="Due date" />
      <Pill size="xs" startIcon={<Icon icon={CalendarOutline} />} label="Aug 28, 2026" />
    </div>
  );
}
```

### As a picker trigger

Pass `render={<MenuTrigger />}` (or a `PopoverTrigger`) and the pill opens the picker. While the popup is up the trigger carries Base UI's `data-popup-open` and wears its active look, so the open property reads as pressed for as long as its menu is showing. The reverse graft, `<MenuTrigger render={<Pill … />}>`, works the same way. `SelectTrigger` and `ComboboxTrigger` are not graft targets: use their `pill` variant when a Select or Combobox needs the same compact label anatomy and active-open treatment, without an automatic chevron.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { Pill } from "@makeplane/propel/components/pill";
import { PriorityHigh, PriorityLow, PriorityMedium } from "@makeplane/propel/icons";

export default function MenuTriggerDemo() {
  return (
    <Menu>
      <Pill
        size="sm"
        startIcon={<Icon icon={PriorityHigh} tint="danger" />}
        label="High"
        render={<MenuTrigger />}
      />
      <MenuContent>
        <MenuItem icon={<Icon icon={PriorityHigh} tint="danger" />} label="High" selected />
        <MenuItem icon={<Icon icon={PriorityMedium} tint="secondary" />} label="Medium" />
        <MenuItem icon={<Icon icon={PriorityLow} tint="secondary" />} label="Low" />
      </MenuContent>
    </Menu>
  );
}
```

### Colour swatch

`Swatch` is a filled 8px disc that takes the same box an `Icon` would, so label chips line up with icon chips. `fill` is any CSS colour — the label's stored value, not a token. The same part fits a `MenuItem`'s `icon` slot.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { Pill } from "@makeplane/propel/components/pill";
import { Swatch } from "@makeplane/propel/elements/swatch";
import { ReleaseTagOutline } from "@makeplane/propel/icons";

export default function SwatchDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Pill size="xs" startIcon={<Swatch fill="#e5484d" />} label="Bug" />
      <Pill size="xs" startIcon={<Swatch fill="#3e63dd" />} label="2 labels" />
      <Pill size="xs" startIcon={<Icon icon={ReleaseTagOutline} />} label="Labels" placeholder />
      <Menu>
        <Pill
          size="xs"
          startIcon={<Swatch fill="#30a46c" />}
          label="Feature"
          render={<MenuTrigger />}
        />
        <MenuContent>
          <MenuItem icon={<Swatch fill="#e5484d" />} label="Bug" />
          <MenuItem icon={<Swatch fill="#30a46c" />} label="Feature" selected />
          <MenuItem icon={<Swatch fill="#3e63dd" />} label="Docs" />
        </MenuContent>
      </Menu>
    </div>
  );
}
```

### Label-less triggers

`label` may be omitted when `aria-label` is given: the label part is skipped entirely, so a pill can hold an avatar stack or an icon alone. Omitting both is a type error.

```tsx
import { Avatar } from "@makeplane/propel/components/avatar";
import { AvatarGroup } from "@makeplane/propel/components/avatar-group";
import { Icon } from "@makeplane/propel/components/icon";
import { Pill } from "@makeplane/propel/components/pill";
import { AddOutline } from "@makeplane/propel/icons";

export default function AvatarTriggerDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Pill
        size="xs"
        variant="ghost"
        aria-label="Assignees: Amelia Parker, David Wilson"
        startIcon={
          <AvatarGroup size="2xs" render={<span />}>
            <Avatar fallback="AP" />
            <Avatar fallback="DW" />
          </AvatarGroup>
        }
      />
      <Pill size="xs" placeholder startIcon={<Icon icon={AddOutline} />} label="Assignees" />
    </div>
  );
}
```

### A property row

The compact row of pickers on a work item — state, priority, dates, assignees, labels, cycle — built only from propel parts. One chrome at one size; every leading node fills the same glyph box; colour lives on the glyph and the label stays the pill's text colour; an empty property is a `placeholder`; a filled assignee stack sits in a `ghost` pill so avatars are not boxed.

```tsx
import { Avatar } from "@makeplane/propel/components/avatar";
import { AvatarGroup } from "@makeplane/propel/components/avatar-group";
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { Pill } from "@makeplane/propel/components/pill";
import { Swatch } from "@makeplane/propel/elements/swatch";
import {
  CalendarOutline,
  CircleDashedOutline,
  RepeatOutline,
  PriorityHigh,
} from "@makeplane/propel/icons";

export default function PropertyRowDemo() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Menu>
        <Pill
          size="xs"
          startIcon={<Icon icon={CircleDashedOutline} tint="tertiary" />}
          label="Backlog"
          render={<MenuTrigger />}
        />
        <MenuContent>
          <MenuItem
            icon={<Icon icon={CircleDashedOutline} tint="secondary" />}
            label="Backlog"
            selected
          />
          <MenuItem icon={<Icon icon={CircleDashedOutline} tint="secondary" />} label="Todo" />
        </MenuContent>
      </Menu>
      <Menu>
        <Pill
          size="xs"
          startIcon={<Icon icon={PriorityHigh} tint="danger" />}
          label="High"
          render={<MenuTrigger />}
        />
        <MenuContent>
          <MenuItem icon={<Icon icon={PriorityHigh} tint="danger" />} label="High" selected />
          <MenuItem icon={<Icon icon={PriorityHigh} tint="secondary" />} label="Low" />
        </MenuContent>
      </Menu>
      <Menu>
        <Pill
          size="xs"
          placeholder
          startIcon={<Icon icon={CalendarOutline} />}
          label="Due date"
          render={<MenuTrigger />}
        />
        <MenuContent>
          <MenuItem label="Today" />
          <MenuItem label="Next week" />
        </MenuContent>
      </Menu>
      <Menu>
        <Pill
          size="xs"
          variant="ghost"
          aria-label="Assignees: Amelia Parker, David Wilson"
          startIcon={
            <AvatarGroup size="2xs" render={<span />}>
              <Avatar fallback="AP" />
              <Avatar fallback="DW" />
            </AvatarGroup>
          }
          render={<MenuTrigger />}
        />
        <MenuContent>
          <MenuItem icon={<Avatar size="2xs" fallback="AP" />} label="Amelia Parker" selected />
          <MenuItem icon={<Avatar size="2xs" fallback="DW" />} label="David Wilson" selected />
        </MenuContent>
      </Menu>
      <Menu>
        <Pill
          size="xs"
          startIcon={<Swatch fill="#e5484d" />}
          label="Bug"
          render={<MenuTrigger />}
        />
        <MenuContent>
          <MenuItem icon={<Swatch fill="#e5484d" />} label="Bug" selected />
          <MenuItem icon={<Swatch fill="#30a46c" />} label="Feature" />
        </MenuContent>
      </Menu>
      <Menu>
        <Pill
          size="xs"
          placeholder
          startIcon={<Icon icon={RepeatOutline} />}
          label="Cycle"
          render={<MenuTrigger />}
        />
        <MenuContent>
          <MenuItem label="Sprint 14" />
          <MenuItem label="Sprint 15" />
        </MenuContent>
      </Menu>
    </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.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Pill } from "@makeplane/propel/components/pill";
import { ReleaseTagOutline } from "@makeplane/propel/icons";

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

### Icon only

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

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { IconPill } from "@makeplane/propel/components/pill";
import { AddOutline, CloseOutline } from "@makeplane/propel/icons";

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

### A connected group

`PillGroup` joins pills that read as one value — a filter condition, a compound property. The segments stay ordinary `Pill`s and `IconPill`s: the frame overlaps their borders into a single divider, keeps only the outermost corners rounded, and lifts whichever segment is hovered, focused, or holding an open picker so it owns the seam it shares. Each segment is still an independent button with its own picker, so the group needs no roving focus and no selection — name it with `aria-label`.

`size` is shared through the group, so a segment can omit it. The scale runs `sm` and `md`: there is no 20px step, because a contiguous segment cannot carry the hit ring the standalone 20px pill uses, and shrinking the target below 24px would fail WCAG 2.5.8. For several independent actions that merely share chrome, reach for `ButtonGroup` instead.

```tsx
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@makeplane/propel/components/menu";
import { IconPill, Pill, PillGroup } from "@makeplane/propel/components/pill";
import { Swatch } from "@makeplane/propel/elements/swatch";
import { BoxesOutline, FilterOutline, CloseOutline } from "@makeplane/propel/icons";

export default function GroupDemo() {
  return (
    <div className="flex items-center gap-2">
      <PillGroup size="md" aria-label="Parent type filter">
        <Menu>
          <Pill
            startIcon={<Icon icon={BoxesOutline} tint="tertiary" />}
            label="Parent type"
            render={<MenuTrigger />}
          />
          <MenuContent>
            <MenuItem label="Parent type" selected />
            <MenuItem label="State" />
          </MenuContent>
        </Menu>
        <Menu>
          <Pill label="is" render={<MenuTrigger />} />
          <MenuContent>
            <MenuItem label="is" selected />
            <MenuItem label="is not" />
          </MenuContent>
        </Menu>
        <Menu>
          <Pill startIcon={<Swatch fill="#3f76ff" />} label="Task" render={<MenuTrigger />} />
          <MenuContent>
            <MenuItem icon={<Swatch fill="#3f76ff" />} label="Task" selected />
            <MenuItem icon={<Swatch fill="#30a46c" />} label="Epic" />
          </MenuContent>
        </Menu>
        <IconPill aria-label="Clear filter" icon={<Icon icon={CloseOutline} />} />
      </PillGroup>
      <IconPill size="md" aria-label="Filter options" icon={<Icon icon={FilterOutline} />} />
    </div>
  );
}
```
## API Reference

### Pill

The ready-made pill button: grafts Base UI's `Button` behavior onto the styled `Pill` 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`). With no `label` the label part is
skipped entirely (an empty flex child would still consume the pill's gap). A consumer `render` —
`render={<MenuTrigger />}` to make the pill open a picker — is threaded onto the styled element
so the trigger behavior and its `data-popup-open` land on the real `<button>`; the reverse graft
(`<MenuTrigger render={<Pill … />}>`) works as well. `nativeButton` rides along the way it does
on `Button`, for a `render` that is not a button.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<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. |
| `size` | `"xs" \| "sm" \| "md"` | — | Scale. Required, except inside a `PillGroup` — the group shares its own `size` with every segment, so a segment omits it. Omitting it outside a group throws rather than guessing a rung. |
| `variant` | `"outline" \| "soft" \| "ghost"` | `outline` | Fill treatment. `outline` (≈ Button secondary) is the bordered chip; `soft` (≈ Button tertiary) is borderless on a filled layer; `ghost` is borderless and transparent at rest, for a trigger whose content is its own box (an avatar stack). |
| `placeholder` | `boolean` | `false` | Marks a trigger with no value yet: dims the label and any inherited-tint glyph to the placeholder colour until a value is chosen (still dim while the picker is open). |
| `startIcon` | `ReactNode` | — | Element rendered before the label (inline-start), e.g. `<Icon icon={Tag} />` or `<Swatch fill="…" />`. |
| `endIcon` | `ReactNode` | — | Element rendered after the label (inline-end), e.g. `<Icon icon={X} />`. |
| `loading` | `boolean` | `false` | Busy state: hides start/end icons, shows a spinner after the label, and blocks clicks. |
| `nativeButton` | `boolean` | — | Set `false` when `render` swaps the underlying tag away from `<button>` (an `<a href>`, a `<div>`): Base UI then adds `role="button"`, tab focus, and Enter/Space activation, and the element's `type="button"` default is withheld — it is valid on nothing but a button. |
| `label` | `string` | — |  |
| `aria-label` | `string` | — |  |

### 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. A consumer `render`
(`render={<MenuTrigger />}`) is threaded onto the styled element, so the trigger behavior and its
`data-popup-open` land on the real `<button>` and the pill wears its open look.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `icon` (required) | `ReactNode` | — | The icon element, usually `<Icon icon={...} />`. |
| `aria-label` | `string` | — | Defines a string value that labels the current element. Required: icon-only pills have no visible text, so they must be labeled. @see aria-labelledby. |
| `render` | `ReactElement<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. |
| `size` | `"xs" \| "sm" \| "md"` | — | Scale. Required, except inside a `PillGroup` — the group shares its own `size` with every segment, so a segment omits it. Omitting it outside a group throws rather than guessing a rung. |
| `loading` | `boolean` | `false` | Busy state: swaps the icon for a spinner and blocks clicks. |
| `nativeButton` | `boolean` | — | Set `false` when `render` swaps the underlying tag away from `<button>` (an `<a href>`, a `<span>`): Base UI then adds `role="button"`, tab focus, and Enter/Space activation, and the element's `type="button"` default is withheld — it is valid on nothing but a button. |

### PillGroup

The ready-made connected pill group: a `group`-role frame that shares `size` with every `Pill`
and `IconPill` inside it via context (a segment's own `size` still wins). There is no Base UI
group primitive — the segments are independent Base UI `Button`s, each free to open its own
picker; the frame contributes only the shared chrome, the shared rung, and the role. Name the
group for assistive tech via `aria-label`.

Built for a compound control whose parts read as one value — a filter condition (`Parent type ·
is · Task · ✕`), a property row. For several independent actions that merely share chrome, reach
for `ButtonGroup`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` (required) | `"sm" \| "md"` | — |  |
| `render` | `ReactElement<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. |

### Swatch

A filled 8px colour disc centred in the inherited `--node-size` box, so it sits exactly where an
icon would: the leading node of a label pill, or a `MenuItem`'s `icon` slot. Decorative
(`aria-hidden`) — the adjacent label carries the name. `fill` is the one styling input a consumer
supplies, because the colour is data rather than a token; the part applies it itself as
`--swatch-fill`, so no `style` prop is exposed.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `fill` (required) | `string` | — | The disc's colour — any CSS `<color>` (a label's stored hex, an `rgb()` string). |
| `render` | `ReactElement<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. |
