component
A radio button component for single selection from a group. Supports three appearances: radio (default circular indicator), button (Panel-powered with full emphasis and feedback), and tab (minimal underline tab — a 2px accent bar on the bottom edge, or the inline-start edge with orientation="vertical").
---
import Radio from "../Radio.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "column" })}>
<Radio id="astro-def-1" name="astro-def" value="option1">Option 1</Radio>
<Radio id="astro-def-2" name="astro-def" value="option2" checked
>Option 2</Radio
>
<Radio id="astro-def-3" name="astro-def" value="option3" disabled
>Disabled</Radio
>
</div>All four appearance variants side by side.
---
import Radio from "../Radio.astro";
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-app-r1" name="astro-app-radio" value="r1" checked
>Option 1</Radio
>
<Radio id="astro-app-r2" name="astro-app-radio" value="r2">Option 2</Radio
>
</div>
</div>
<div>
<h3>button</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-app-b1"
name="astro-app-button"
value="b1"
appearance="button"
checked
>
Option 1
</Radio>
<Radio
id="astro-app-b2"
name="astro-app-button"
value="b2"
appearance="button"
>
Option 2
</Radio>
<Radio
id="astro-app-b3"
name="astro-app-button"
value="b3"
appearance="button"
>
Option 3
</Radio>
</div>
</div>
<div>
<h3>tab (horizontal)</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-app-th1"
name="astro-app-tabh"
value="th1"
appearance="tab"
checked
>
Tab 1
</Radio>
<Radio
id="astro-app-th2"
name="astro-app-tabh"
value="th2"
appearance="tab"
>
Tab 2
</Radio>
<Radio
id="astro-app-th3"
name="astro-app-tabh"
value="th3"
appearance="tab"
>
Tab 3
</Radio>
</div>
</div>
<div>
<h3>tab (vertical)</h3>
<div class={stack({ gap: "xs", direction: "column" })}>
<Radio
id="astro-app-tv1"
name="astro-app-tabv"
value="tv1"
appearance="tab"
orientation="vertical"
checked
>
Dashboard
</Radio>
<Radio
id="astro-app-tv2"
name="astro-app-tabv"
value="tv2"
appearance="tab"
orientation="vertical"
>
Settings
</Radio>
</div>
</div>
</div>Three sizes are available across all appearances.
---
import Radio from "../Radio.astro";
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-size-sm" name="astro-size" value="sm" size="sm" checked
>Small</Radio
>
<Radio id="astro-size-md" name="astro-size" value="md" size="md"
>Medium</Radio
>
<Radio id="astro-size-lg" name="astro-size" value="lg" size="lg"
>Large</Radio
>
</div>
</div>
<div>
<h3>button appearance</h3>
<div class={stack({ gap: "sm", direction: "row", align: "center" })}>
<Radio
id="astro-sbtn-sm"
name="astro-sbtn"
value="sm"
appearance="button"
size="sm"
checked>Small</Radio
>
<Radio
id="astro-sbtn-md"
name="astro-sbtn"
value="md"
appearance="button"
size="md">Medium</Radio
>
<Radio
id="astro-sbtn-lg"
name="astro-sbtn"
value="lg"
appearance="button"
size="lg">Large</Radio
>
</div>
</div>
</div>appearance="button" composes with Panel — use feedback (primary, success, danger, warning, neutral) to control the surface.
---
import Radio from "../Radio.astro";
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-btn-p1"
name="astro-btn-primary"
value="p1"
appearance="button"
checked
>
Option 1
</Radio>
<Radio
id="astro-btn-p2"
name="astro-btn-primary"
value="p2"
appearance="button"
>
Option 2
</Radio>
<Radio
id="astro-btn-p3"
name="astro-btn-primary"
value="p3"
appearance="button"
>
Option 3
</Radio>
</div>
</div>
<div>
<h3>Danger</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-btn-d1"
name="astro-btn-danger"
value="d1"
appearance="button"
feedback="danger"
checked
>
Option 1
</Radio>
<Radio
id="astro-btn-d2"
name="astro-btn-danger"
value="d2"
appearance="button"
feedback="danger"
>
Option 2
</Radio>
</div>
</div>
<div>
<h3>Warning</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-btn-w1"
name="astro-btn-warning"
value="w1"
appearance="button"
feedback="warning"
checked
>
Option 1
</Radio>
<Radio
id="astro-btn-w2"
name="astro-btn-warning"
value="w2"
appearance="button"
feedback="warning"
>
Option 2
</Radio>
</div>
</div>
<div>
<h3>Success</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-btn-s1"
name="astro-btn-success"
value="s1"
appearance="button"
feedback="success"
checked
>
Option 1
</Radio>
<Radio
id="astro-btn-s2"
name="astro-btn-success"
value="s2"
appearance="button"
feedback="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.
---
import Radio from "../Radio.astro";
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-ebtn-p1"
name="astro-ebtn-primary"
value="p1"
appearance="button"
checked
>
Option 1
</Radio>
<Radio
id="astro-ebtn-p2"
name="astro-ebtn-primary"
value="p2"
appearance="button"
>
Option 2
</Radio>
</div>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-ebtn-pn1"
name="astro-ebtn-primary-neutral"
value="pn1"
appearance="button"
feedback="neutral"
checked
>
Neutral 1
</Radio>
<Radio
id="astro-ebtn-pn2"
name="astro-ebtn-primary-neutral"
value="pn2"
appearance="button"
feedback="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-ebtn-s1"
name="astro-ebtn-secondary"
value="s1"
appearance="button"
emphasis="secondary"
checked
>
Option 1
</Radio>
<Radio
id="astro-ebtn-s2"
name="astro-ebtn-secondary"
value="s2"
appearance="button"
emphasis="secondary"
>
Option 2
</Radio>
</div>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-ebtn-sn1"
name="astro-ebtn-secondary-neutral"
value="sn1"
appearance="button"
emphasis="secondary"
feedback="neutral"
checked
>
Neutral 1
</Radio>
<Radio
id="astro-ebtn-sn2"
name="astro-ebtn-secondary-neutral"
value="sn2"
appearance="button"
emphasis="secondary"
feedback="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-ebtn-t1"
name="astro-ebtn-tertiary"
value="t1"
appearance="button"
emphasis="tertiary"
checked
>
Option 1
</Radio>
<Radio
id="astro-ebtn-t2"
name="astro-ebtn-tertiary"
value="t2"
appearance="button"
emphasis="tertiary"
>
Option 2
</Radio>
</div>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-ebtn-tn1"
name="astro-ebtn-tertiary-neutral"
value="tn1"
appearance="button"
emphasis="tertiary"
feedback="neutral"
checked
>
Neutral 1
</Radio>
<Radio
id="astro-ebtn-tn2"
name="astro-ebtn-tertiary-neutral"
value="tn2"
appearance="button"
emphasis="tertiary"
feedback="neutral"
>
Neutral 2
</Radio>
</div>
</div>
</div>
<div>
<h3>Secondary + feedback</h3>
<div class={stack({ gap: "sm", direction: "row" })}>
<Radio
id="astro-ebtn-fbn"
name="astro-ebtn-feedback"
value="fbn"
appearance="button"
emphasis="secondary"
feedback="neutral"
checked
>
Neutral
</Radio>
<Radio
id="astro-ebtn-fbd"
name="astro-ebtn-feedback"
value="fbd"
appearance="button"
emphasis="secondary"
feedback="danger"
>
Danger
</Radio>
<Radio
id="astro-ebtn-fbw"
name="astro-ebtn-feedback"
value="fbw"
appearance="button"
emphasis="secondary"
feedback="warning"
>
Warning
</Radio>
<Radio
id="astro-ebtn-fbs"
name="astro-ebtn-feedback"
value="fbs"
appearance="button"
emphasis="secondary"
feedback="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 feedback to change the accent color. Wrap tabs in a Group to get the 1px baseline track drawn for you — 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.
---
import Radio from "../Radio.astro";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { GitBranch, Trash2 } from "@lucide/astro";
import { css } from "@pindoba/styled-system/css";
import { stack } from "@pindoba/styled-system/patterns";
// The 1px baseline track normally comes from a wrapping <Group> (or a
// Navigation/TabNav strip); the demo draws it on the plain wrapper instead.
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-tab-h1"
name="astro-tab-horiz"
value="h1"
appearance="tab"
checked
>
All
</Radio>
<Radio
id="astro-tab-h2"
name="astro-tab-horiz"
value="h2"
appearance="tab"
>
Active
</Radio>
<Radio
id="astro-tab-h3"
name="astro-tab-horiz"
value="h3"
appearance="tab"
>
Archived
</Radio>
</div>
</div>
<div>
<h3>Tab — Vertical orientation</h3>
<div class={vTrack}>
<Radio
id="astro-tab-v1"
name="astro-tab-vert"
value="v1"
appearance="tab"
orientation="vertical"
checked
>
Dashboard
</Radio>
<Radio
id="astro-tab-v2"
name="astro-tab-vert"
value="v2"
appearance="tab"
orientation="vertical"
>
Settings
</Radio>
<Radio
id="astro-tab-v3"
name="astro-tab-vert"
value="v3"
appearance="tab"
orientation="vertical"
>
Profile
</Radio>
</div>
</div>
<div>
<h3>Tab — Danger feedback</h3>
<div class={hTrack}>
<Radio
id="astro-tab-hd1"
name="astro-tab-danger"
value="hd1"
appearance="tab"
feedback="danger"
checked
>
Errors
</Radio>
<Radio
id="astro-tab-hd2"
name="astro-tab-danger"
value="hd2"
appearance="tab"
feedback="danger"
>
Warnings
</Radio>
</div>
</div>
<div>
<h3>Tab — Background</h3>
<!-- Opt-in resting fill on the neutral ramp; the default is transparent. -->
<div class={hTrack}>
<Radio
id="astro-tab-b1"
name="astro-tab-bg"
value="b1"
appearance="tab"
background="surface.peak"
checked
>
Inbox
</Radio>
<Radio
id="astro-tab-b2"
name="astro-tab-bg"
value="b2"
appearance="tab"
background="surface.peak"
>
Archive
</Radio>
</div>
</div>
<div>
<h3>Tab — Leading and trailing slots</h3>
<!-- Tab rows are on the Control Var Contract: a leading Stamp and a
trailing Badge size themselves to the row metrics, like Button. -->
<div class={hTrack}>
<Radio
id="astro-tab-s1"
name="astro-tab-slots"
value="s1"
appearance="tab"
checked
>
<Stamp slot="leading" emphasis="ghost">
<GitBranch />
</Stamp>
Active
<Badge slot="trailing" size="sm">221</Badge>
</Radio>
<Radio
id="astro-tab-s2"
name="astro-tab-slots"
value="s2"
appearance="tab"
feedback="danger"
>
<Stamp slot="leading" emphasis="ghost">
<Trash2 />
</Stamp>
Deleted
<Badge slot="trailing" size="sm" feedback="danger">0</Badge>
</Radio>
</div>
</div>
</div>Badges inside radio components scale with the component’s size via fontSize: 1rem.
---
import Radio from "../Radio.astro";
import PindobaBadge 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-badge-radio-sm"
name="astro-badge-radio"
value="sm"
size="sm"
checked
>
Small <PindobaBadge feedback="primary" size="sm">3</PindobaBadge>
</Radio>
<Radio
id="astro-badge-radio-md"
name="astro-badge-radio"
value="md"
size="md"
>
Medium <PindobaBadge feedback="primary" size="sm">3</PindobaBadge>
</Radio>
<Radio
id="astro-badge-radio-lg"
name="astro-badge-radio"
value="lg"
size="lg"
>
Large <PindobaBadge feedback="primary" size="sm">3</PindobaBadge>
</Radio>
</div>
</div>
<div>
<h3>button appearance</h3>
<div class={stack({ gap: "sm", direction: "row", align: "center" })}>
<Radio
id="astro-badge-btn-sm"
name="astro-badge-btn"
value="sm"
appearance="button"
size="sm"
checked
>
Small <PindobaBadge feedback="primary" size="sm">3</PindobaBadge>
</Radio>
<Radio
id="astro-badge-btn-md"
name="astro-badge-btn"
value="md"
appearance="button"
size="md"
>
Medium <PindobaBadge feedback="primary" size="sm">3</PindobaBadge>
</Radio>
<Radio
id="astro-badge-btn-lg"
name="astro-badge-btn"
value="lg"
appearance="button"
size="lg"
>
Large <PindobaBadge feedback="primary" size="sm">3</PindobaBadge>
</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.
---
import Radio from "../Radio.astro";
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-lt-rbtn-${size}`}
appearance="button"
name="lt-demo-button"
value={size}
size={size}
fullWidth
checked={size === "md"}
>
<Stamp slot="leading" emphasis="adaptive" size="xs" shape="circle">
<Mail />
</Stamp>
Unread
<Badge slot="trailing" emphasis="adaptive" size="sm">
7
</Badge>
</Radio>
))
}
</div>
</div>
<div>
<h3>button appearance — leading only</h3>
<div class={stack({ gap: "sm", direction: "column" })}>
{
sizes.map((size) => (
<Radio
id={`astro-lt-rbtn-leading-${size}`}
appearance="button"
name="lt-demo-button-leading"
value={size}
size={size}
fullWidth
checked={size === "md"}
>
<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-lt-rbtn-trailing-${size}`}
appearance="button"
name="lt-demo-button-trailing"
value={size}
size={size}
fullWidth
checked={size === "md"}
>
Unread
<Badge slot="trailing" emphasis="adaptive" size="sm">
7
</Badge>
</Radio>
))
}
</div>
</div>
<div>
<h3>radio appearance</h3>
<div class={stack({ gap: "sm", direction: "column" })}>
{
sizes.map((size) => (
<Radio
id={`astro-lt-r-${size}`}
name="lt-demo-radio"
value={size}
size={size}
checked={size === "md"}
>
Pro plan
<Badge slot="trailing" emphasis="adaptive" size="sm">
$9/mo
</Badge>
</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.
---
import Radio from "../Radio.astro";
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-cust-1"
name="astro-cust"
value="c1"
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-cust-2"
name="astro-cust"
value="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-cust-3"
name="astro-cust"
value="c3"
passThrough={{
root: {
props: { "data-testid": "my-radio" },
},
input: {
props: { "aria-label": "Custom radio button" },
},
}}
>
With data attributes
</Radio>
</div>
</div>
</div>Visual appearance variant of the control.
"label"Root element tag. Set `"span"` for a label-less indicator.
Surface background from the Panel scale (`surface.peak` → `surface.ground`, or `transparent`).
Resting border for `appearance="button"`. Secondary and tertiary default to `"muted"` and promote to `"default"` when checked; primary defaults to `"default"`. Use `"none"` for a borderless look or `"bold"`/`"accent"` to intensify.
No description yet.
No description yet.
primaryVisual emphasis / prominence level.
"primary"Color scheme applied via `colorPalette`. For the `radio` and `tab` appearances this sets the indicator accent color; for `button` appearance it sets the Panel feedback color.
Stretch the component to fill the available width.
No description yet.
No description yet.
"horizontal"Bar edge for `appearance="tab"` — `"horizontal"` puts the 2px indicator on the bottom edge (a tab strip), `"vertical"` on the inline-start edge (a side rail). Ignored by other appearances.
Per-slot style and attribute override bag. Each slot accepts a `style` (Panda `SystemStyleObject` merged with base styles) and a `props` (HTML attributes spread onto the element).
Corner radius of the `appearance="button"` or `appearance="tab"` surface. Both default to the size-derived radius from the control sizing scale; pass `"inner"` to compute a concentric radius from a parent Panel's exported `--panel-out-radius`/`--panel-out-padding` (this is how `Choice` keeps its segmented items nested inside the group container). Tab bar-edge corners (bottom when horizontal, inline-start when vertical) always stay square so the accent bar sits seated on the track.
Size variant.
No description yet.
Render the button surface with a translucent (semi-transparent) background.
Plus all standard <input> HTML
attributes.