component

Button

A versatile button component that supports multiple visual styles, sizes, and tone states. Panel powers all surface rendering — giving buttons full access to backgrounds, borders, translucency, and radius controls without reimplementing them.

Emphasis

Four visual emphasis levels: primary uses a solid accent background, secondary uses a neutral surface background with a border, ghost is transparent until you hover it, and adaptive is transparent and inherits its text color from the parent — useful for buttons placed inside a colored panel or banner that should match the surrounding tone.

---
// GENERATED from packages/demos/button/emphasis.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Button emphasis="primary">Primary</Button>
  <Button emphasis="secondary">Secondary</Button>
  <Button emphasis="ghost">Ghost</Button>
  <Button emphasis="adaptive">Adaptive</Button>
</div>

Adaptive in context

emphasis="adaptive" shines when the button lives inside a colored container — the button picks up the parent’s colorPalette instead of imposing its own. The same trick works tonally on any other emphasis via tone="inherit".

emphasis="adaptive" — transparent surface, inherits text color from each panel.

tone="neutral"
tone="primary"
tone="success"
tone="warning"
tone="danger"

emphasis="primary" tone="inherit" — keeps its filled treatment, but the accent surface follows the parent panel's palette.

tone="neutral"
tone="primary"
tone="success"
tone="warning"
tone="danger"
---
import Button from "@pindoba/astro-button";
import Panel from "@pindoba/astro-panel";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const feedbacks = [
  "neutral",
  "primary",
  "success",
  "warning",
  "danger",
] as const;
---

<div class={stack({ gap: "lg", direction: "column", align: "stretch" })}>
  <div class={stack({ gap: "sm", direction: "column", align: "stretch" })}>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      <strong>emphasis="adaptive"</strong> — transparent surface, inherits text
      color from each panel.
    </p>
    <div
      class={stack({
        gap: "sm",
        direction: "row",
        align: "stretch",
        flexWrap: "wrap",
      })}
    >
      {feedbacks.map((tone) => (
        <Panel tone={tone} padding="md" radius="lg" border="muted">
          <div
            class={stack({
              gap: "2xs",
              direction: "column",
              align: "flex-start",
            })}
          >
            <span
              class={css({
                fontSize: "xs",
                color: "panel.text.muted",
                textTransform: "uppercase",
                letterSpacing: "wider",
              })}
            >
              tone="{tone}"
            </span>
            <Button emphasis="adaptive" size="sm">
              Take action
            </Button>
          </div>
        </Panel>
      ))}
    </div>
  </div>

  <div class={stack({ gap: "sm", direction: "column", align: "stretch" })}>
    <p class={css({ fontSize: "sm", color: "panel.text.muted" })}>
      <strong>emphasis="primary" tone="inherit"</strong> — keeps its filled
      treatment, but the accent surface follows the parent panel's palette.
    </p>
    <div
      class={stack({
        gap: "sm",
        direction: "row",
        align: "stretch",
        flexWrap: "wrap",
      })}
    >
      {feedbacks.map((tone) => (
        <Panel tone={tone} padding="md" radius="lg" border="muted">
          <div
            class={stack({
              gap: "2xs",
              direction: "column",
              align: "flex-start",
            })}
          >
            <span
              class={css({
                fontSize: "xs",
                color: "panel.text.muted",
                textTransform: "uppercase",
                letterSpacing: "wider",
              })}
            >
              tone="{tone}"
            </span>
            <Button emphasis="primary" tone="inherit" size="sm">
              Confirm
            </Button>
          </div>
        </Panel>
      ))}
    </div>
  </div>
</div>

Tone

Apply semantic tone colors across all emphasis levels. The default tone uses the primary color palette.

Primary emphasis

Secondary emphasis

Ghost emphasis

---
// GENERATED from packages/demos/button/tone.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Primary emphasis</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button emphasis="primary">Default</Button>
      <Button emphasis="primary" tone="success">
        Success
      </Button>
      <Button emphasis="primary" tone="danger">
        Danger
      </Button>
      <Button emphasis="primary" tone="warning">
        Warning
      </Button>
    </div>
  </div>

  <div>
    <h3>Secondary emphasis</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button emphasis="secondary">Default</Button>
      <Button emphasis="secondary" tone="primary">
        Primary
      </Button>
      <Button emphasis="secondary" tone="success">
        Success
      </Button>
      <Button emphasis="secondary" tone="danger">
        Danger
      </Button>
      <Button emphasis="secondary" tone="warning">
        Warning
      </Button>
    </div>
  </div>

  <div>
    <h3>Ghost emphasis</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button emphasis="ghost">Default</Button>
      <Button emphasis="ghost" tone="primary">
        Primary
      </Button>
      <Button emphasis="ghost" tone="success">
        Success
      </Button>
      <Button emphasis="ghost" tone="danger">
        Danger
      </Button>
      <Button emphasis="ghost" tone="warning">
        Warning
      </Button>
    </div>
  </div>
</div>

Size

Buttons come in five sizes to accommodate different layout needs.

---
// GENERATED from packages/demos/button/size.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Button size="xs">Extra Small</Button>
  <Button size="sm">Small</Button>
  <Button size="md">Medium</Button>
  <Button size="lg">Large</Button>
  <Button size="xl">Extra Large</Button>
</div>

Shape

The shape prop adds pill, square, or circle variants. Square and circle constrain the button to equal width and height — ideal for icon-only buttons. All shapes maintain their size-proportional height from the shared sizing system.

Shapes

Sizes

---
// GENERATED from packages/demos/button/shape.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Shapes</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button size="md">Default</Button>
      <Button shape="pill" size="md">
        Pill
      </Button>
      <Button aria-label="Bookmark" shape="square" size="md">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
        </svg>
      </Button>
      <Button aria-label="Add" shape="circle" size="md">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
      </Button>
    </div>
  </div>

  <div>
    <h3>Sizes</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button shape="pill" size="xs">
        Pill xs
      </Button>
      <Button shape="pill" size="sm">
        Pill sm
      </Button>
      <Button shape="pill" size="md">
        Pill md
      </Button>
      <Button shape="pill" size="lg">
        Pill lg
      </Button>
    </div>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
        mt: "sm",
      })}
    >
      <Button aria-label="Bookmark" shape="square" size="xs">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
        </svg>
      </Button>
      <Button aria-label="Bookmark" shape="square" size="sm">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
        </svg>
      </Button>
      <Button aria-label="Bookmark" shape="square" size="md">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
        </svg>
      </Button>
      <Button aria-label="Bookmark" shape="square" size="lg">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
        </svg>
      </Button>
    </div>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
        mt: "sm",
      })}
    >
      <Button aria-label="Add" shape="circle" size="xs">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
      </Button>
      <Button aria-label="Add" shape="circle" size="sm">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
      </Button>
      <Button aria-label="Add" shape="circle" size="md">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
      </Button>
      <Button aria-label="Add" shape="circle" size="lg">
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
      </Button>
    </div>
  </div>
</div>

Border

Control the border style using the border prop. secondary buttons default to border="muted". All border styles use box-shadow and do not affect button dimensions.

Secondary with border

Ghost with border

---
// GENERATED from packages/demos/button/border.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Secondary with border</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button emphasis="secondary" border="none">
        None
      </Button>
      <Button emphasis="secondary" border="default">
        Default
      </Button>
      <Button emphasis="secondary" border="bold">
        Bold
      </Button>
      <Button emphasis="secondary" border="muted">
        Muted
      </Button>
    </div>
  </div>

  <div>
    <h3>Ghost with border</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button emphasis="ghost" border="none">
        None
      </Button>
      <Button emphasis="ghost" border="default">
        Default
      </Button>
      <Button emphasis="ghost" border="bold">
        Bold
      </Button>
      <Button emphasis="ghost" border="muted">
        Muted
      </Button>
    </div>
  </div>
</div>

Border Radius

Override the default size-proportional border radius with the radius prop. Granular edge and corner props (radiusTop, radiusLeft, radiusTopLeft, etc.) allow asymmetric rounding for grouped or nested layouts.

Border radius

Partial radius

---
// GENERATED from packages/demos/button/radius.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Border radius</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button radius="none">None</Button>
      <Button radius="sm">Small</Button>
      <Button radius="md">Medium</Button>
      <Button radius="xl">XL</Button>
      <Button radius="full">Full</Button>
    </div>
  </div>

  <div>
    <h3>Partial radius</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button radiusTop="full">Top</Button>
      <Button radiusBottom="full">Bottom</Button>
      <Button radiusLeft="full">Left</Button>
      <Button radiusRight="full">Right</Button>
    </div>
  </div>
</div>

Accent & Background

emphasis="primary" uses accent=true by default — a solid full-color (500) background with contrast text. emphasis="secondary" exposes the background prop for surface.peak through surface.ground and transparent options. accent is not available on secondary.

Primary (filled by default)

Secondary with different backgrounds

---
// GENERATED from packages/demos/button/accent.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Primary (filled by default)</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button emphasis="primary">Action</Button>
      <Button emphasis="primary" tone="success">
        Success
      </Button>
      <Button emphasis="primary" tone="danger">
        Danger
      </Button>
      <Button emphasis="primary" tone="warning">
        Warning
      </Button>
    </div>
  </div>

  <div>
    <h3>Secondary with different backgrounds</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button emphasis="secondary" background="surface.peak">
        Surface 1
      </Button>
      <Button emphasis="secondary" background="surface.base">
        Surface 3
      </Button>
      <Button emphasis="secondary" background="surface.ground">
        Surface 5
      </Button>
      <Button emphasis="secondary" background="transparent">
        Transparent
      </Button>
    </div>
  </div>
</div>

Icons & addons (Affix)

Compose icons, stamps, or badges as children wrapped in <Affix>: side="start" (default) places the addon before the label, side="end" after it, pushed to the far edge. The button’s edge padding tightens automatically on the affix side, and row-aware addons (Badge, Stamp, Kbd) size themselves to the control row. Inner badges or stamps using emphasis="adaptive" follow the button’s text color across all button emphases.

Both slots

Leading only

Trailing only

---
import Button from "@pindoba/astro-button";
import Affix from "@pindoba/astro-affix";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Download } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";

const sizes = ["xs", "sm", "md", "lg"] as const;
---

<div class={stack({ gap: "lg", direction: "column", align: "flex-start" })}>
  <div class={stack({ gap: "md", direction: "column", align: "flex-start" })}>
    <h4>Both slots</h4>
    {sizes.map((size) => (
      <Button emphasis="primary" size={size}>
        <Affix>
          <Stamp
            emphasis="primary"
            tone="primary"
            background="surface.ground"
            border="none"
          >
            <Download />
          </Stamp>
        </Affix>
        Download
        <Affix side="end">
          <Badge>2.3 MB</Badge>
        </Affix>
      </Button>
    ))}
  </div>

  <div class={stack({ gap: "md", direction: "column", align: "flex-start" })}>
    <h4>Leading only</h4>
    {sizes.map((size) => (
      <Button emphasis="primary" size={size}>
        <Affix>
          <Stamp
            emphasis="primary"
            tone="primary"
            background="surface.ground"
            border="none"
          >
            <Download />
          </Stamp>
        </Affix>
        Download
      </Button>
    ))}
  </div>

  <div class={stack({ gap: "md", direction: "column", align: "flex-start" })}>
    <h4>Trailing only</h4>
    {sizes.map((size) => (
      <Button emphasis="primary" size={size}>
        Download
        <Affix side="end">
          <Badge>2.3 MB</Badge>
        </Affix>
      </Button>
    ))}
  </div>
</div>

Declarative leading / trailing

For the common case, pass leading and trailing instead of composing <Affix> children: the button renders the affix boxes for you, with the same row-aware sizing and padding. Style the boxes through passThrough.leading / passThrough.trailing. Put icons inside a <Stamp emphasis="ghost">. The two buttons below render identically:

---
// GENERATED from packages/demos/button/declarative-affix.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import Affix from "@pindoba/astro-affix";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Download } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Button>
    <Stamp slot="leading" emphasis="ghost">
      <Download />
    </Stamp>
    <Badge slot="trailing" emphasis="secondary">
      2.3 MB
    </Badge>
    Declarative
  </Button>

  <Button>
    <Affix>
      <Stamp emphasis="ghost">
        <Download />
      </Stamp>
    </Affix>
    Imperative
    <Affix side="end">
      <Badge emphasis="secondary">2.3 MB</Badge>
    </Affix>
  </Button>
</div>

Some actions look like a button but navigate — open a page, a mailto:, or WhatsApp in a new tab. Pass href and the button renders an <a> with the exact same recipe: sizes, emphasis and tone, affixes, focus ring and disabled look are pixel-identical to a <button>. There is deliberately no as prop: a <button> and an <a> are the only elements this control can honestly be, and href is the signal that picks between them — an arbitrary as="div" would render a button-looking element with no keyboard or assistive-technology semantics.

  • Attributes. Anchor attributes (target, rel, download, hreflang, referrerpolicy, ping) are forwarded. Button-only attributes (type, form*, name, value, popovertarget) are typed away in link mode and never emitted — an <a> gets no type.
  • New tabs. target="_blank" adds rel="noopener noreferrer" unless you set rel yourself (any value, including an empty one, is kept).
  • Disabled. An <a> has no disabled state, so a disabled link drops its href (nothing can activate or open it — not Enter, a click, middle-click or “open in new tab”), keeps role="link" so it is still announced as a link, and gets aria-disabled="true" and tabindex="-1", taking it out of the tab order.
  • Keyboard. Links activate on Enter only. Space scrolls the page, as it does for every native link — that is expected, not a bug. If the action must also respond to Space, it is a button: drop href and handle onclick.
---
// GENERATED from packages/demos/button/link.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Button href="https://wa.me/" target="_blank">
    Chat on WhatsApp
  </Button>
  <Button href="/components/affix/" emphasis="secondary">
    Read about Affix
  </Button>
  <Button href="/favicon.svg" download="pindoba.svg" emphasis="ghost">
    Download logo
  </Button>
  <Button href="/components/button/" disabled>
    Disabled link
  </Button>
</div>

Custom Styling

The passThrough prop provides two escape hatches for advanced customization: style accepts any Panda CSS SystemStyleObject applied directly to the button root, and props forwards arbitrary HTML attributes. Use style for layout overrides and props for accessibility attributes like aria-label or data-testid.

Custom style via passThrough

Custom props via passThrough

---
// GENERATED from packages/demos/button/custom.demo.tsx — edit the source, not this file.
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Custom style via passThrough</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button
        passThrough={{
          root: {
            style: { minWidth: "200px", justifyContent: "start" },
          },
        }}
      >
        Wide left-aligned
      </Button>
    </div>
  </div>

  <div>
    <h3>Custom props via passThrough</h3>
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      <Button
        emphasis="secondary"
        passThrough={{
          root: {
            props: {
              "aria-label": "Submit form",
              "data-testid": "submit-btn",
            },
          },
        }}
      >
        Submit
      </Button>
    </div>
  </div>
</div>
  • Components
  • Blocks