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.
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>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.
emphasis="primary" tone="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((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>Apply semantic tone colors across all emphasis levels. The default tone uses the primary color palette.
---
// 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>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>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.
---
// 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>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.
---
// 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>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.
---
// 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>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.
---
// 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>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"
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>leading / trailingFor 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.
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.target="_blank" adds rel="noopener noreferrer" unless you set rel yourself (any value, including an empty one, is kept).<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.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>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.
---
// 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>