A radio button component for single selection from a group. Supports three appearances: radio (default circular indicator), button (Panel-powered with full emphasis and tone), and tab (minimal underline tab — a 2px accent bar on the bottom edge, or the inline-start edge with orientation="vertical").
---
// GENERATED from packages/demos/radio/default.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column" })}>
<Radio id="astro-demo-def-1" name="astro-demo-default" value="option1">
Option 1
</Radio>
<Radio
id="astro-demo-def-2"
name="astro-demo-default"
value="option2"
checked
>
Option 2
</Radio>
<Radio
id="astro-demo-def-3"
name="astro-demo-default"
value="option3"
disabled
>
Disabled
</Radio>
</div>All four appearance variants side by side.
---
// GENERATED from packages/demos/radio/appearance.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>radio (default)</h3>
<div class={stack({ gap: "md", direction: "column" })}>
<Radio id="astro-demo-app-r1" name="astro-demo-app-radio" value="app-r1">
Option 1
</Radio>
<Radio
id="astro-demo-app-r2"
name="astro-demo-app-radio"
value="app-r2"
checked
>
Option 2
</Radio>
</div>
</div>
<div>
<h3>button</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-app-b1"
name="astro-demo-app-button"
value="app-b1"
appearance="button"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-app-b2"
name="astro-demo-app-button"
value="app-b2"
appearance="button"
>
Option 2
</Radio>
<Radio
id="astro-demo-app-b3"
name="astro-demo-app-button"
value="app-b3"
appearance="button"
>
Option 3
</Radio>
</div>
</div>
<div>
<h3>tab (horizontal)</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-app-th1"
name="astro-demo-app-tab-h"
value="app-th1"
appearance="tab"
checked
>
Tab 1
</Radio>
<Radio
id="astro-demo-app-th2"
name="astro-demo-app-tab-h"
value="app-th2"
appearance="tab"
>
Tab 2
</Radio>
<Radio
id="astro-demo-app-th3"
name="astro-demo-app-tab-h"
value="app-th3"
appearance="tab"
>
Tab 3
</Radio>
</div>
</div>
<div>
<h3>tab (vertical)</h3>
<div class={stack({ gap: "xs", direction: "column" })}>
<Radio
id="astro-demo-app-tv1"
name="astro-demo-app-tab-v"
value="app-tv1"
appearance="tab"
orientation="vertical"
checked
>
Tab 1
</Radio>
<Radio
id="astro-demo-app-tv2"
name="astro-demo-app-tab-v"
value="app-tv2"
appearance="tab"
orientation="vertical"
>
Tab 2
</Radio>
</div>
</div>
</div>Three sizes are available across all appearances.
---
// GENERATED from packages/demos/radio/size.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>radio appearance</h3>
<div class={stack({ gap: "sm", direction: "row", align: "center" })}>
<Radio
id="astro-demo-size-sm"
name="astro-demo-size-radio"
value="size-sm"
size="sm"
checked
>
Small
</Radio>
<Radio
id="astro-demo-size-md"
name="astro-demo-size-radio"
value="size-md"
size="md"
>
Medium
</Radio>
<Radio
id="astro-demo-size-lg"
name="astro-demo-size-radio"
value="size-lg"
size="lg"
>
Large
</Radio>
</div>
</div>
<div>
<h3>button appearance</h3>
<div class={stack({ gap: "sm", direction: "row", align: "center" })}>
<Radio
id="astro-demo-sbtn-sm"
name="astro-demo-size-button"
value="sbtn-sm"
appearance="button"
size="sm"
checked
>
Small
</Radio>
<Radio
id="astro-demo-sbtn-md"
name="astro-demo-size-button"
value="sbtn-md"
appearance="button"
size="md"
>
Medium
</Radio>
<Radio
id="astro-demo-sbtn-lg"
name="astro-demo-size-button"
value="sbtn-lg"
appearance="button"
size="lg"
>
Large
</Radio>
</div>
</div>
</div>appearance="button" composes with Panel — use tone (primary, success, danger, warning, neutral) to control the surface.
---
// GENERATED from packages/demos/radio/button.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Primary (default)</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-btn-p1"
name="astro-demo-btn-primary"
value="btn-p1"
appearance="button"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-btn-p2"
name="astro-demo-btn-primary"
value="btn-p2"
appearance="button"
>
Option 2
</Radio>
<Radio
id="astro-demo-btn-p3"
name="astro-demo-btn-primary"
value="btn-p3"
appearance="button"
>
Option 3
</Radio>
</div>
</div>
<div>
<h3>Danger</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-btn-d1"
name="astro-demo-btn-danger"
value="btn-d1"
appearance="button"
tone="danger"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-btn-d2"
name="astro-demo-btn-danger"
value="btn-d2"
appearance="button"
tone="danger"
>
Option 2
</Radio>
</div>
</div>
<div>
<h3>Warning</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-btn-w1"
name="astro-demo-btn-warning"
value="btn-w1"
appearance="button"
tone="warning"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-btn-w2"
name="astro-demo-btn-warning"
value="btn-w2"
appearance="button"
tone="warning"
>
Option 2
</Radio>
</div>
</div>
<div>
<h3>Success</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-btn-s1"
name="astro-demo-btn-success"
value="btn-s1"
appearance="button"
tone="success"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-btn-s2"
name="astro-demo-btn-success"
value="btn-s2"
appearance="button"
tone="success"
>
Option 2
</Radio>
</div>
</div>
</div>For appearance="button", the emphasis prop controls how the unchecked and checked states relate to each other. primary (default) keeps today’s filled-accent look. secondary rides the colorPalette.surface.* ramp and swaps to the opposite ramp position when checked. tertiary stays on a neutral surface with accent-colored text and a muted border. The background prop still picks the starting position; hover and active step through the ramp from there.
The border prop owns the resting border. Secondary and tertiary default to "muted" and promote to "default" on the checked state; primary defaults to "default" everywhere (and the accent ring on checked is part of primary’s filled identity). Pass border="none" to remove the resting border entirely (great with tertiary for a borderless segmented look) or border="bold" | "accent" to intensify it.
---
// GENERATED from packages/demos/radio/emphasis.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Primary (default)</h3>
<div class={stack({ gap: "lg", direction: "row" })}>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-ebtn-p1"
name="astro-demo-ebtn-p"
value="ebtn-p1"
appearance="button"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-ebtn-p2"
name="astro-demo-ebtn-p"
value="ebtn-p2"
appearance="button"
>
Option 2
</Radio>
</div>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-ebtn-pn1"
name="astro-demo-ebtn-pn"
value="ebtn-pn1"
appearance="button"
tone="neutral"
checked
>
Neutral 1
</Radio>
<Radio
id="astro-demo-ebtn-pn2"
name="astro-demo-ebtn-pn"
value="ebtn-pn2"
appearance="button"
tone="neutral"
>
Neutral 2
</Radio>
</div>
</div>
</div>
<div>
<h3>Secondary</h3>
<div class={stack({ gap: "lg", direction: "row" })}>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-ebtn-s1"
name="astro-demo-ebtn-s"
value="ebtn-s1"
appearance="button"
emphasis="secondary"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-ebtn-s2"
name="astro-demo-ebtn-s"
value="ebtn-s2"
appearance="button"
emphasis="secondary"
>
Option 2
</Radio>
</div>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-ebtn-sn1"
name="astro-demo-ebtn-sn"
value="ebtn-sn1"
appearance="button"
emphasis="secondary"
tone="neutral"
checked
>
Neutral 1
</Radio>
<Radio
id="astro-demo-ebtn-sn2"
name="astro-demo-ebtn-sn"
value="ebtn-sn2"
appearance="button"
emphasis="secondary"
tone="neutral"
>
Neutral 2
</Radio>
</div>
</div>
</div>
<div>
<h3>Tertiary</h3>
<div class={stack({ gap: "lg", direction: "row" })}>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-ebtn-t1"
name="astro-demo-ebtn-t"
value="ebtn-t1"
appearance="button"
emphasis="tertiary"
checked
>
Option 1
</Radio>
<Radio
id="astro-demo-ebtn-t2"
name="astro-demo-ebtn-t"
value="ebtn-t2"
appearance="button"
emphasis="tertiary"
>
Option 2
</Radio>
</div>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-ebtn-tn1"
name="astro-demo-ebtn-tn"
value="ebtn-tn1"
appearance="button"
emphasis="tertiary"
tone="neutral"
checked
>
Neutral 1
</Radio>
<Radio
id="astro-demo-ebtn-tn2"
name="astro-demo-ebtn-tn"
value="ebtn-tn2"
appearance="button"
emphasis="tertiary"
tone="neutral"
>
Neutral 2
</Radio>
</div>
</div>
</div>
<div>
<h3>Secondary + tone</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-demo-ebtn-fbn"
name="astro-demo-ebtn-fb"
value="ebtn-fbn"
appearance="button"
emphasis="secondary"
tone="neutral"
checked
>
Neutral
</Radio>
<Radio
id="astro-demo-ebtn-fbd"
name="astro-demo-ebtn-fb"
value="ebtn-fbd"
appearance="button"
emphasis="secondary"
tone="danger"
>
Danger
</Radio>
<Radio
id="astro-demo-ebtn-fbw"
name="astro-demo-ebtn-fb"
value="ebtn-fbw"
appearance="button"
emphasis="secondary"
tone="warning"
>
Warning
</Radio>
<Radio
id="astro-demo-ebtn-fbs"
name="astro-demo-ebtn-fb"
value="ebtn-fbs"
appearance="button"
emphasis="secondary"
tone="success"
>
Success
</Radio>
</div>
</div>
</div>appearance="tab" renders a minimal underline tab: transparent surface and a 2px inset accent bar on the checked item. orientation picks the bar edge — "horizontal" (default, bottom edge) or "vertical" (inline-start edge). Use tone to change the accent color. Wrap tabs in a Group to get the 1px baseline track drawn for you, plus a selection bar that slides between tabs (CSS anchor positioning; without it, each tab keeps its own static bar). Navigation and Tab build their strips on this same appearance.
Tab rows are sized control rows on the Control Var Contract, like the button appearance: leading/trailing content (a <Stamp> icon, a <Badge> count) sizes itself to the row metrics. Tabs are transparent by default with the size-derived control radius on the free corners; pass background (a neutral surface token, e.g. "surface.peak") for an opt-in resting fill, and radius to retune or square the corners — hover, press, and the checked state keep moving only the bar and text, and the bar-edge corners always stay square so the accent bar sits seated on the track.
---
// GENERATED from packages/demos/radio/tab.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { GitBranch, Trash } from "@lucide/astro";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
const hTrack = `${stack({ gap: "sm", direction: "row" })} ${css({
boxShadow: "0 -1px 0 0 token(colors.neutral.border) inset",
})}`;
const vTrack = `${stack({ gap: "xs", direction: "column" })} ${css({
boxShadow: "1px 0 0 0 token(colors.neutral.border) inset",
width: "max-content",
})}`;
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Tab</h3>
<div class={hTrack}>
<Radio
id="astro-demo-tab-h1"
name="astro-demo-tab-h"
value="tab-h1"
appearance="tab"
checked
>
All
</Radio>
<Radio
id="astro-demo-tab-h2"
name="astro-demo-tab-h"
value="tab-h2"
appearance="tab"
>
Active
</Radio>
<Radio
id="astro-demo-tab-h3"
name="astro-demo-tab-h"
value="tab-h3"
appearance="tab"
>
Archived
</Radio>
</div>
</div>
<div>
<h3>Tab — Vertical orientation</h3>
<div class={vTrack}>
<Radio
id="astro-demo-tab-v1"
name="astro-demo-tab-v"
value="tab-v1"
appearance="tab"
orientation="vertical"
checked
>
Dashboard
</Radio>
<Radio
id="astro-demo-tab-v2"
name="astro-demo-tab-v"
value="tab-v2"
appearance="tab"
orientation="vertical"
>
Settings
</Radio>
<Radio
id="astro-demo-tab-v3"
name="astro-demo-tab-v"
value="tab-v3"
appearance="tab"
orientation="vertical"
>
Profile
</Radio>
</div>
</div>
<div>
<h3>Tab — Danger tone</h3>
<div class={hTrack}>
<Radio
id="astro-demo-tab-hd1"
name="astro-demo-tab-hd"
value="tab-hd1"
appearance="tab"
tone="danger"
checked
>
Errors
</Radio>
<Radio
id="astro-demo-tab-hd2"
name="astro-demo-tab-hd"
value="tab-hd2"
appearance="tab"
tone="danger"
>
Warnings
</Radio>
</div>
</div>
<div>
<h3>Tab — Background</h3>
<div class={hTrack}>
<Radio
id="astro-demo-tab-b1"
name="astro-demo-tab-bg"
value="tab-b1"
appearance="tab"
background="surface.peak"
checked
>
Inbox
</Radio>
<Radio
id="astro-demo-tab-b2"
name="astro-demo-tab-bg"
value="tab-b2"
appearance="tab"
background="surface.peak"
>
Archive
</Radio>
</div>
</div>
<div>
<h3>Tab — Leading and trailing slots</h3>
<div class={hTrack}>
<Radio
id="astro-demo-tab-s1"
name="astro-demo-tab-slots"
value="tab-s1"
appearance="tab"
checked
>
<Stamp slot="leading" emphasis="ghost">
<GitBranch />
</Stamp>
<Badge slot="trailing" size="sm">
221
</Badge>
Active
</Radio>
<Radio
id="astro-demo-tab-s2"
name="astro-demo-tab-slots"
value="tab-s2"
appearance="tab"
tone="danger"
>
<Stamp slot="leading" emphasis="ghost">
<Trash />
</Stamp>
<Badge slot="trailing" size="sm" tone="danger">
0
</Badge>
Deleted
</Radio>
</div>
</div>
</div>Badges inside radio components scale with the component’s size via fontSize: 1rem.
---
// GENERATED from packages/demos/radio/badge.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import Badge from "@pindoba/astro-badge";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>radio appearance</h3>
<div class={stack({ gap: "sm", direction: "row", align: "center" })}>
<Radio
id="astro-demo-badge-radio-sm"
name="astro-demo-badge-radio"
value="badge-radio-sm"
size="sm"
checked
>
Small{" "}
<Badge tone="primary" size="sm">
3
</Badge>
</Radio>
<Radio
id="astro-demo-badge-radio-md"
name="astro-demo-badge-radio"
value="badge-radio-md"
size="md"
>
Medium{" "}
<Badge tone="primary" size="sm">
3
</Badge>
</Radio>
<Radio
id="astro-demo-badge-radio-lg"
name="astro-demo-badge-radio"
value="badge-radio-lg"
size="lg"
>
Large{" "}
<Badge tone="primary" size="sm">
3
</Badge>
</Radio>
</div>
</div>
<div>
<h3>button appearance</h3>
<div class={stack({ gap: "sm", direction: "row", align: "center" })}>
<Radio
id="astro-demo-badge-btn-sm"
name="astro-demo-badge-btn"
value="badge-btn-sm"
appearance="button"
size="sm"
checked
>
Small{" "}
<Badge tone="primary" size="sm">
3
</Badge>
</Radio>
<Radio
id="astro-demo-badge-btn-md"
name="astro-demo-badge-btn"
value="badge-btn-md"
appearance="button"
size="md"
>
Medium{" "}
<Badge tone="primary" size="sm">
3
</Badge>
</Radio>
<Radio
id="astro-demo-badge-btn-lg"
name="astro-demo-badge-btn"
value="badge-btn-lg"
appearance="button"
size="lg"
>
Large{" "}
<Badge tone="primary" size="sm">
3
</Badge>
</Radio>
</div>
</div>
</div>Use the leading and trailing slots to position icons, stamps, or badges at the start and end of the label. With emphasis="adaptive", badges and stamps follow the parent’s color in both unchecked and checked states. The trailing slot pins to the end of the row, which pairs well with fullWidth.
---
// GENERATED from packages/demos/radio/leading-trailing.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Mail } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
const sizes = ["sm", "md", "lg"] as const;
---
<div class={stack({ gap: "lg", direction: "column" })}>
<div>
<h3>button appearance — both slots</h3>
<div class={stack({ gap: "sm", direction: "column" })}>
{sizes.map((size) => (
<Radio
id={`astro-demo-lt-rbtn-${size}`}
appearance="button"
name="astro-demo-lt-demo-button"
value={size}
size={size}
checked={size === "md"}
fullWidth
>
<Stamp slot="leading" emphasis="adaptive" size="xs" shape="circle">
<Mail />
</Stamp>
<Badge slot="trailing" emphasis="adaptive" size="sm">
7
</Badge>
Unread
</Radio>
))}
</div>
</div>
<div>
<h3>button appearance — leading only</h3>
<div class={stack({ gap: "sm", direction: "column" })}>
{sizes.map((size) => (
<Radio
id={`astro-demo-lt-rbtn-leading-${size}`}
appearance="button"
name="astro-demo-lt-demo-button-leading"
value={size}
size={size}
checked={size === "md"}
fullWidth
>
<Stamp slot="leading" emphasis="adaptive" size="xs" shape="circle">
<Mail />
</Stamp>
Unread
</Radio>
))}
</div>
</div>
<div>
<h3>button appearance — trailing only</h3>
<div class={stack({ gap: "sm", direction: "column" })}>
{sizes.map((size) => (
<Radio
id={`astro-demo-lt-rbtn-trailing-${size}`}
appearance="button"
name="astro-demo-lt-demo-button-trailing"
value={size}
size={size}
checked={size === "md"}
fullWidth
>
<Badge slot="trailing" emphasis="adaptive" size="sm">
7
</Badge>
Unread
</Radio>
))}
</div>
</div>
<div>
<h3>radio appearance</h3>
<div class={stack({ gap: "sm", direction: "column" })}>
{sizes.map((size) => (
<Radio
id={`astro-demo-lt-r-${size}`}
name="astro-demo-lt-demo-radio"
value={size}
size={size}
checked={size === "md"}
>
<Badge slot="trailing" emphasis="adaptive" size="sm">
$9/mo
</Badge>
Pro plan
</Radio>
))}
</div>
</div>
</div>The passThrough prop provides per-slot escape hatches: style accepts a Panda CSS SystemStyleObject merged with the base styles, and props forwards arbitrary HTML attributes onto the element.
---
// GENERATED from packages/demos/radio/custom.demo.tsx — edit the source, not this file.
import Radio from "@pindoba/astro-radio";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "xl", direction: "column" })}>
<div>
<h3>Custom root style via passThrough</h3>
<div class={stack({ gap: "md", direction: "column" })}>
<Radio
id="astro-demo-cust-1"
name="astro-demo-custom"
value="cust-c1"
checked
passThrough={{
root: {
style: { gap: "lg", padding: "sm" },
},
}}
>
Extra padding
</Radio>
</div>
</div>
<div>
<h3>Custom input style via passThrough</h3>
<div class={stack({ gap: "md", direction: "column" })}>
<Radio
id="astro-demo-cust-2"
name="astro-demo-custom"
value="cust-c2"
passThrough={{
input: {
style: { width: "2em", height: "2em" },
},
}}
>
Larger indicator
</Radio>
</div>
</div>
<div>
<h3>Custom props via passThrough</h3>
<div class={stack({ gap: "md", direction: "column" })}>
<Radio
id="astro-demo-cust-3"
name="astro-demo-custom"
value="cust-c3"
passThrough={{
root: {
props: { "data-testid": "my-radio" },
},
input: {
props: { "aria-label": "Custom radio button" },
},
}}
>
With data attributes
</Radio>
</div>
</div>
</div>