HTML-First Components

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.

The Principle

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.

Native Elements in Action

Native Checkboxes & Radios with Custom Appearance

Built on appearance: none and the :checked pseudo-class. No JavaScript needed.

Tabs Using Radio Inputs

Tab navigation built on <input type="radio"> and the :checked selector. Zero JavaScript.

Tab content goes here. Each panel is controlled by the radio input state.

Native <details> Disclosure

Built-in expand/collapse behavior without JavaScript.

What is HTML-first?
HTML-first means leveraging native HTML elements and their built-in behaviors before reaching for JavaScript solutions.
Why avoid reimplementing native elements?
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.
---
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 &amp; 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)}
        >&lt;input type="radio"&gt;</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)}>&lt;details&gt;</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>

Checkboxes as Toggle Buttons

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.

Radio Inputs for Tabs

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.

Details for Disclosure

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>

Dialog for Modals

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.

Default Props Are Not Overwritten

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>

When JavaScript Is Necessary

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.

Best Practices

  1. Start with the native element — can <button>, <input>, <select>, <details>, or <dialog> do what you need? Use it
  2. Style with CSS, not JavaScript:checked, :focus-visible, :disabled, [open], and ::backdrop replace most UI state logic
  3. Don’t add role to elements that already have it — a <button> doesn’t need role="button"
  4. Don’t prevent default behavior — if you’re calling e.preventDefault() on a native element, consider whether you’re fighting the platform
  5. Use <form> for data collection — forms give you validation, submission, and reset for free