component
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 five 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>
<Button size="xl">Extra 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.peak through surface.ground 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.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>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.
---
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"
feedback="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"
feedback="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>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>falseUse 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.
"surface.peak"Background style — only available when `emphasis="secondary"`. `surface.peak`: lightest. `surface.hill–3`: intermediate. `surface.ground`: darkest. `transparent`: no background.
"none" ("muted" for emphasis="secondary")Border style. `none`: no border. `default`: standard border. `bold`: stronger emphasis. `muted`: subtle separation.
No description yet.
Bound reference to the underlying `<button>` element (`bind:this`).
"primary"Visual emphasis level. `primary`: solid accent background with contrast text. `secondary`: neutral surface with a border. `ghost`: transparent until hovered. `adaptive`: transparent and inherits text color from the parent — pair with `feedback="inherit"` to fully follow the context.
"primary" when emphasis="primary", "inherit" when emphasis="adaptive", "neutral" otherwiseSemantic color tone. `neutral`: greyscale. `primary`: brand palette. `success`: green. `warning`: orange. `danger`: red. `inherit`: keep the ambient `colorPalette` from the parent unchanged — combine with `emphasis="adaptive"` for a fully context-following button.
No description yet.
Per-slot style and HTML-attribute overrides (`root`). The slot accepts `style` (a Panda style object merged into the slot's class) and `props` (extra attributes spread onto the element). Icon/addon boxes are composed as `<Affix>` children, which carry their own `passThrough`.
Border-radius override. Accepts the full spacing scale plus `full` for fully rounded corners. Overrides the default size-proportional radius.
Border radius for the bottom-left and bottom-right corners.
Border radius for the top-left and bottom-left corners.
Border radius for the top-right and bottom-right corners.
Border radius for the top-left and top-right corners.
Shape variant. `pill`: fully rounded corners. `circle`: equal width/height circle. `square`: equal width/height square.
"md"Size variant. Shares the same height system as inputs.
falseConstrain a child SVG to the button's content box (`width`/`height: 100%`), useful for icon-only buttons.
No description yet.
falseApply a frosted-glass effect with backdrop blur. Works with surface backgrounds.
Plus all standard <button> HTML
attributes.