Pindoba follows a simple rule: use the HTML element that already does what you need. We don’t overwrite default behavior, we don’t reimplement browser features with JavaScript, and we don’t fight the platform.
Browsers ship with interactive elements that handle keyboard navigation, focus management, form submission, and accessibility announcements. Reimplementing these with <div onClick> or custom JavaScript is error-prone, less accessible, and more code to maintain.
Built on appearance: none and the
:checked pseudo-class. No JavaScript needed.
Tab navigation built on <input type="radio"> and the :checked selector. Zero JavaScript.
<details> Disclosure
Built-in expand/collapse behavior without JavaScript.
---
import { css } from "@pindoba/styled-system/css";
import Checkbox from "@pindoba/astro-checkbox";
import Radio from "@pindoba/astro-radio";
import { Tab } from "@pindoba/astro-tab";
const containerStyle = css({
display: "flex",
flexDirection: "column",
gap: "lg",
});
const sectionStyle = css.raw({
backgroundColor: "neutral.surface",
borderRadius: "lg",
padding: "lg",
border: "1px solid",
borderColor: "neutral.border.muted",
});
const sectionTitleStyle = css.raw({
fontSize: "md",
fontWeight: "bold",
marginBottom: "sm",
color: "neutral.text.bold",
});
const descriptionStyle = css.raw({
fontSize: "sm",
color: "neutral.text",
marginBottom: "md",
});
const codeStyle = css.raw({
fontFamily: "mono",
fontSize: "xs",
backgroundColor: "neutral.surface.hover",
padding: "2xs xs",
borderRadius: "sm",
color: "neutral.text.bold",
});
const rowStyle = css.raw({
display: "flex",
gap: "md",
flexWrap: "wrap",
alignItems: "center",
});
const detailsStyle = css.raw({
borderRadius: "md",
border: "1px solid",
borderColor: "neutral.border.muted",
backgroundColor: "neutral.surface",
overflow: "hidden",
});
const summaryStyle = css.raw({
p: "token(spacing.sm) token(spacing.md)",
cursor: "pointer",
fontWeight: "medium",
color: "neutral.text.bold",
fontSize: "sm",
"details[open] &": {
borderBottom: "1px solid token(colors.neutral.border.muted)",
},
_hover: {
backgroundColor: "neutral.surface.hover",
},
});
---
<div class={containerStyle}>
<div class={css(sectionStyle)}>
<h3 class={css(sectionTitleStyle)}>
Native Checkboxes & Radios with Custom Appearance
</h3>
<p class={css(descriptionStyle)}>
Built on <code class={css(codeStyle)}>appearance: none</code> and the
<code class={css(codeStyle)}>:checked</code> pseudo-class. No JavaScript needed.
</p>
<div class={css({ display: "flex", flexDirection: "column", gap: "md" })}>
<div class={css(rowStyle)}>
<Checkbox name="demo-checkbox" checked>Option A</Checkbox>
<Checkbox name="demo-checkbox">Option B</Checkbox>
<Checkbox name="demo-checkbox">Option C</Checkbox>
</div>
<div class={css(rowStyle)}>
<Radio id="choice-1" name="demo-radio" checked>Choice 1</Radio>
<Radio id="choice-2" name="demo-radio">Choice 2</Radio>
<Radio id="choice-3" name="demo-radio">Choice 3</Radio>
</div>
</div>
</div>
<div class={css(sectionStyle)}>
<h3 class={css(sectionTitleStyle)}>Tabs Using Radio Inputs</h3>
<p class={css(descriptionStyle)}>
Tab navigation built on <code class={css(codeStyle)}
><input type="radio"></code
> and the <code class={css(codeStyle)}>:checked</code> selector. Zero JavaScript.
</p>
<Tab
items={[
{ id: "tab1", label: "Tab 1" },
{ id: "tab2", label: "Tab 2" },
{ id: "tab3", label: "Tab 3" },
]}
defaultTab="tab1"
>
<div slot="tab1" class={css({ fontSize: "sm", color: "neutral.text" })}>
Tab content goes here. Each panel is controlled by the radio input
state.
</div>
<div slot="tab2" class={css({ fontSize: "sm", color: "neutral.text" })}>
Tab 2 content.
</div>
<div slot="tab3" class={css({ fontSize: "sm", color: "neutral.text" })}>
Tab 3 content.
</div>
</Tab>
</div>
<div class={css(sectionStyle)}>
<h3 class={css(sectionTitleStyle)}>
Native <code class={css(codeStyle)}><details></code> Disclosure
</h3>
<p class={css(descriptionStyle)}>
Built-in expand/collapse behavior without JavaScript.
</p>
<div class={css({ display: "flex", flexDirection: "column", gap: "sm" })}>
<details class={css(detailsStyle)}>
<summary class={css(summaryStyle)}>What is HTML-first?</summary>
<div
class={css({
p: "token(spacing.sm) token(spacing.md)",
fontSize: "sm",
color: "neutral.text",
})}
>
HTML-first means leveraging native HTML elements and their built-in
behaviors before reaching for JavaScript solutions.
</div>
</details>
<details class={css(detailsStyle)}>
<summary class={css(summaryStyle)}
>Why avoid reimplementing native elements?</summary
>
<div
class={css({
p: "token(spacing.sm) token(spacing.md)",
fontSize: "sm",
color: "neutral.text",
})}
>
Native elements come with built-in accessibility, keyboard support,
and form integration. Reimplementing them with divs and JavaScript is
error-prone and often misses edge cases.
</div>
</details>
</div>
</div>
</div>HTML checkboxes already behave like toggle buttons — they have an on/off state, respond to click and keyboard, participate in forms, and announce their state to screen readers. Pindoba styles them with CSS appearance: none and the :checked pseudo-class:
import { css } from "@pindoba/styled-system/css";
<label className={css({ display: "inline-flex", alignItems: "center", gap: "xs", cursor: "pointer" })}>
<input
type="checkbox"
className={css({
appearance: "none",
width: "5",
height: "5",
borderRadius: "sm",
border: "1px solid",
borderColor: "neutral.border.muted",
backgroundColor: "neutral.surface",
cursor: "pointer",
_checked: {
backgroundColor: "primary.sunken",
borderColor: "primary.border",
},
})}
/>
Enable notifications
</label>
What you get for free: Space key toggles, form submission includes the value, screen readers announce state, :checked pseudo-class for styling.
A tab component is a set of mutually exclusive options — exactly what radio inputs do. Pindoba’s tab component is built on <input type="radio">:
<div role="tablist">
<input type="radio" name="tabs" id="tab-general" checked
className={css({ position: "absolute", opacity: "0", width: "0", height: "0" })} />
<label htmlFor="tab-general" className={css({
padding: "sm lg",
cursor: "pointer",
borderBottom: "2px solid transparent",
_peerChecked: { color: "primary.text.bold", borderBottomColor: "primary.border" },
})}>
General
</label>
</div>
What you get for free: only one tab selected at a time, arrow keys navigate, form integration, no state management code.
The <details> element provides expand/collapse with zero JavaScript:
---
import { css } from "@pindoba/styled-system/css";
---
<details class={css({ borderRadius: "md", border: "1px solid", borderColor: "neutral.border.muted" })}>
<summary class={css({ padding: "sm md", cursor: "pointer", fontWeight: "medium" })}>
Advanced settings
</summary>
<div class={css({ padding: "sm md" })}>
Content revealed on click.
</div>
</details>
Pindoba uses the native <dialog> element. The core-dialog package adds scroll lock and focus trap on top of it, but the rendered element is always <dialog>:
<dialog className={css({
borderRadius: "lg",
padding: "lg",
border: "none",
backgroundColor: "neutral.surface",
color: "neutral.text.bold",
"::backdrop": { backgroundColor: "neutral.sunken" },
})}>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
What you get for free: focus trapping, Escape to close, backdrop blocks interaction, method="dialog" closes on submit.
Pindoba components spread user props onto the underlying HTML element. All native attributes pass through:
<!-- All native attributes work as expected -->
<Button type="submit" form="my-form" disabled>Submit</Button>
<Button type="button" popovertarget="my-popover">Toggle</Button>
Some interactions genuinely require JavaScript — dynamic positioning (popovers), scroll locking (dialogs), and complex drag-and-drop. In those cases, Pindoba’s core packages provide the minimal JavaScript needed while still rendering native HTML elements.
<button>, <input>, <select>, <details>, or <dialog> do what you need? Use it:checked, :focus-visible, :disabled, [open], and ::backdrop replace most UI state logicrole to elements that already have it — a <button> doesn’t need role="button"e.preventDefault() on a native element, consider whether you’re fighting the platform<form> for data collection — forms give you validation, submission, and reset for free