component

Button

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

shape
size
Label
Emphasis
Feedback
Background
Border
Icon as label
disabled
translucent
Leading icon
Trailing icon

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.

---
import Button from "../Button.astro";
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 feedback="inherit".

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

feedback="neutral"
feedback="primary"
feedback="success"
feedback="warning"
feedback="danger"

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

feedback="neutral"
feedback="primary"
feedback="success"
feedback="warning"
feedback="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((feedback) => (
          <Panel feedback={feedback} 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",
                })}
              >
                feedback="{feedback}"
              </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" feedback="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((feedback) => (
          <Panel feedback={feedback} 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",
                })}
              >
                feedback="{feedback}"
              </span>
              <Button emphasis="primary" feedback="inherit" size="sm">
                Confirm
              </Button>
            </div>
          </Panel>
        ))
      }
    </div>
  </div>
</div>

Feedback

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

Primary emphasis

Secondary emphasis

Ghost emphasis

---
import Button from "../Button.astro";
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" feedback="success">Success</Button>
      <Button emphasis="primary" feedback="danger">Danger</Button>
      <Button emphasis="primary" feedback="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" feedback="primary">Primary</Button>
      <Button emphasis="secondary" feedback="success">Success</Button>
      <Button emphasis="secondary" feedback="danger">Danger</Button>
      <Button emphasis="secondary" feedback="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" feedback="primary">Primary</Button>
      <Button emphasis="ghost" feedback="success">Success</Button>
      <Button emphasis="ghost" feedback="danger">Danger</Button>
      <Button emphasis="ghost" feedback="warning">Warning</Button>
    </div>
  </div>
</div>

Size

Buttons come in four sizes to accommodate different layout needs.

---
import Button from "../Button.astro";
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>
</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

---
import Button from "../Button.astro";
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 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 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 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 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 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 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 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 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 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 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

---
import Button from "../Button.astro";
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

---
import Button from "../Button.astro";
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.soft through surface.deep and transparent options. accent is not available on secondary.

Primary (filled by default)

Secondary with different backgrounds

---
import Button from "../Button.astro";
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" feedback="success">Success</Button>
      <Button emphasis="primary" feedback="danger">Danger</Button>
      <Button emphasis="primary" feedback="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.soft">Surface 1</Button>
      <Button emphasis="secondary" background="surface.step.2">Surface 3</Button
      >
      <Button emphasis="secondary" background="surface.deep">Surface 5</Button>
      <Button emphasis="secondary" background="transparent">Transparent</Button>
    </div>
  </div>
</div>

Leading & Trailing slots

Use the leading and trailing slots to position icons, stamps, or badges at the start and end of the button. 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 Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Icon } from "astro-icon/components";
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}>
          <Stamp
            slot="leading"
            emphasis="adaptive"
            size="xs"
            shape="circle"
            iconFill
          >
            <Icon name="lucide:download" />
          </Stamp>
          Download
          <Badge slot="trailing" emphasis="adaptive" size="sm">
            2.3 MB
          </Badge>
        </Button>
      ))
    }
  </div>

  <div class={stack({ gap: "md", direction: "column", align: "flex-start" })}>
    <h4>Leading only</h4>
    {
      sizes.map((size) => (
        <Button emphasis="primary" size={size}>
          <Stamp
            slot="leading"
            emphasis="adaptive"
            size="xs"
            shape="circle"
            iconFill
          >
            <Icon name="lucide:download" />
          </Stamp>
          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
          <Badge slot="trailing" emphasis="adaptive" size="sm">
            2.3 MB
          </Badge>
        </Button>
      ))
    }
  </div>
</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

---
import Button from "../Button.astro";
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>

Props

props · 19 total
prop type default req description
emphasis "primary""secondary""ghost""adaptive" "primary" Visual emphasis level. primary: Solid accent background with contrast text. secondary: Neutral surface background with a border. ghost: Transparent until hovered. adaptive: Transparent and inherits te…

emphasis

Visual emphasis level. primary: Solid accent background with contrast text. secondary: Neutral surface background with a border. ghost: Transparent until hovered. adaptive: Transparent and inherits text color from the parent — pair with feedback="inherit" to fully follow the surrounding context.

Type "primary" | "secondary" | "ghost" | "adaptive"
Default "primary"
Required No
feedback "neutral""primary""success""warning""danger""inherit" "primary" when emphasis="primary", "inherit" when emphasis="adaptive", "neutral" otherwise Semantic color tone. neutral: Greyscale. primary: Brand palette. success: Green. warning: Orange. danger: Red. inherit: Keep the ambient colorPalette from the parent context unchanged — combine with e…

feedback

Semantic color tone. neutral: Greyscale. primary: Brand palette. success: Green. warning: Orange. danger: Red. inherit: Keep the ambient colorPalette from the parent context unchanged — combine with emphasis="adaptive" for a fully context-following button.

Type "neutral" | "primary" | "success" | "warning" | "danger" | "inherit"
Default "primary" when emphasis="primary", "inherit" when emphasis="adaptive", "neutral" otherwise
Required No
size "xs""sm""md""lg" "md" Size variant. All sizes share the same height system as inputs. xs: Extra small. sm: Small. md: Default. lg: Large.
shape "pill""circle""square" undefined Shape variant. pill: Fully rounded corners. circle: Equal width/height circle. square: Equal width/height square.
background "surface.soft""surface.step.1""surface.step.2""surface.step.3""surface.deep""transparent" "surface.soft" Background style — only available when emphasis="secondary". surface.soft: Lightest surface. surface.step.1-4: Intermediate levels. surface.deep: Darkest surface. transparent: No background.
accent boolean false Use the full-color (500) accent background from the current feedback color with contrast text. Only available on primary and ghost emphasis — primary sets this to true by default.
border "none""default""bold""muted" "none" (secondary: "muted") Box-shadow border style. none: No border. default: Standard border. bold: Stronger emphasis using a bolder color. muted: Subtle separation. Defaults to muted for secondary, none otherwise.
translucent boolean false Apply a frosted glass effect with backdrop blur. Works with surface backgrounds.
radius "none""5xs""4xs""3xs""2xs""xs""sm""md""lg""xl""2xl""3xl""4xl""5xl""6xl""7xl""8xl""9xl""10xl""11xl""full" undefined Border radius override. Accepts the full spacing scale from none to 11xl, plus full for fully rounded corners. Overrides the default size-proportional radius.
radiusTop SpacingScale undefined Border radius for top-left and top-right corners.
radiusBottom SpacingScale undefined Border radius for bottom-left and bottom-right corners.
radiusLeft SpacingScale undefined Border radius for top-left and bottom-left corners.
radiusRight SpacingScale undefined Border radius for top-right and bottom-right corners.
radiusTopLeft SpacingScale undefined Border radius for the top-left corner only.
radiusTopRight SpacingScale undefined Border radius for the top-right corner only.
radiusBottomLeft SpacingScale undefined Border radius for the bottom-left corner only.
radiusBottomRight SpacingScale undefined Border radius for the bottom-right corner only.
passThrough { root?: { style?: SystemStyleObject; props?: Record<string, unknown> } } undefined Custom styling and props for button elements
...rest HTMLAttributes<HTMLButtonElement> - Standard HTML button attributes