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.
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>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.
emphasis="primary" feedback="inherit" — keeps its filled treatment, but the accent surface follows the parent panel's palette.
---
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>Apply semantic feedback colors across all emphasis levels. The default feedback uses the primary color palette.
---
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>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>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.
---
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>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.
---
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>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.
---
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>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.
---
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>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.
---
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>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.
---
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>| 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…
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 Default Required | |
| 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…
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 Default Required | |
| 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 |