component

Input

A text input component with feedback color states, size variants that share the same sizing system as the button component, and composable parts — <InputRoot> + <InputField> — for building inputs with icons and addons.

Feedback

Apply semantic feedback colors to indicate validation state or context.

---
import Input from "../Input.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column" })}>
  <Input feedback="neutral" placeholder="Neutral" />
  <Input feedback="primary" placeholder="Primary" />
  <Input feedback="success" placeholder="Success" />
  <Input feedback="danger" placeholder="Danger" />
  <Input feedback="warning" placeholder="Warning" />
</div>

Size

Inputs come in four sizes that share the same height system as buttons.

---
import Input from "../Input.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column" })}>
  <Input size="xs" placeholder="Extra Small" />
  <Input size="sm" placeholder="Small" />
  <Input size="md" placeholder="Medium" />
  <Input size="lg" placeholder="Large" />
</div>

Icons & addons (Affix)

To add icons, stamps, or badges inside the frame, compose the parts: <InputRoot> carries the chrome (size, feedback, background, border) and <InputField> is the bare <input>; wrap each addon in <Affix>. side="start" (default) places the addon before the field, side="end" after it, pushed to the far edge. The frame’s edge padding tightens automatically on the affix side, row-aware addons (Badge, Stamp, Kbd) size themselves to the control row, and a click anywhere in the frame focuses the field. They scale across every size:

USD
USD
USD
USD
---
import InputRoot from "../InputRoot.astro";
import InputField from "../InputField.astro";
import Affix from "@pindoba/astro-affix";
import Stamp from "@pindoba/astro-stamp";
import Badge from "@pindoba/astro-badge";
import { DollarSign, Search } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";

const sizes = ["xs", "sm", "md", "lg"] as const;
---

<div class={stack({ gap: "md", direction: "column" })}>
  {
    sizes.map((size) => (
      <InputRoot size={size}>
        <Affix>
          <Stamp emphasis="ghost" border="muted">
            <DollarSign />
          </Stamp>
        </Affix>
        <InputField placeholder={`Amount (${size})`} />
        <Affix side="end">
          <Badge emphasis="secondary">USD</Badge>
        </Affix>
      </InputRoot>
    ))
  }
  {
    sizes.map((size) => (
      <InputRoot size={size}>
        <Affix>
          <Stamp emphasis="ghost" border="muted">
            <Search />
          </Stamp>
        </Affix>
        <InputField placeholder={`Search (${size})`} />
      </InputRoot>
    ))
  }
</div>

Interactive buttons (clear, password reveal)

An affix can also hold a real <Button> — a clear button, a password reveal, an inline action. Put the button inside an <Affix> (without decorative, so it stays clickable and visible to assistive technology) and it sizes itself to the row automatically, just like Stamp and Badge. Clicking the button keeps its own focus; clicking anywhere else in the frame still focuses the field. emphasis="ghost" with shape="circle" or shape="square" is the usual look for these affordances:

---
import InputRoot from "../InputRoot.astro";
import InputField from "../InputField.astro";
import Affix from "@pindoba/astro-affix";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { Eye, EyeOff, Search, X } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";

const sizes = ["xs", "sm", "md", "lg"] as const;
---

<div class={stack({ gap: "md", direction: "column" })}>
  <InputRoot>
    <Affix decorative>
      <Stamp emphasis="ghost">
        <Search />
      </Stamp>
    </Affix>
    <InputField
      id="astro-input-affix-clear-field"
      value="Clear me"
      placeholder="Search…"
    />
    <Affix side="end">
      <Button
        id="astro-input-affix-clear-btn"
        emphasis="ghost"
        shape="circle"
        aria-label="Clear"
      >
        <X />
      </Button>
    </Affix>
  </InputRoot>

  <InputRoot>
    <InputField
      id="astro-input-affix-pw-field"
      type="password"
      value="hunter2"
      placeholder="Password"
    />
    <Affix side="end">
      <Button
        id="astro-input-affix-pw-btn"
        emphasis="ghost"
        shape="square"
        aria-label="Show password"
        aria-pressed="false"
      >
        <span data-icon="show"><Eye /></span>
        <span data-icon="hide" hidden><EyeOff /></span>
      </Button>
    </Affix>
  </InputRoot>

  {
    sizes.map((size) => (
      <InputRoot size={size}>
        <InputField value={`Clearable (${size})`} />
        <Affix side="end">
          <Button emphasis="ghost" shape="circle" aria-label="Clear">
            <X />
          </Button>
        </Affix>
      </InputRoot>
    ))
  }
</div>

<style>
  /* The icon-swap spans must not add an inline line box (it would push the
     glyph off the button's flex center). [hidden] keeps working because the
     rule below re-asserts it over the display override. */
  [data-icon] {
    display: inline-flex;
  }
  [data-icon][hidden] {
    display: none;
  }
</style>

<script>
  const clearField = document.getElementById("astro-input-affix-clear-field");
  document
    .getElementById("astro-input-affix-clear-btn")
    ?.addEventListener("click", () => {
      if (clearField instanceof HTMLInputElement) {
        clearField.value = "";
        clearField.focus();
      }
    });

  const pwField = document.getElementById("astro-input-affix-pw-field");
  const pwButton = document.getElementById("astro-input-affix-pw-btn");
  pwButton?.addEventListener("click", () => {
    if (!(pwField instanceof HTMLInputElement) || !pwButton) return;
    const reveal = pwField.type === "password";
    pwField.type = reveal ? "text" : "password";
    pwButton.setAttribute("aria-pressed", String(reveal));
    pwButton.setAttribute(
      "aria-label",
      reveal ? "Hide password" : "Show password",
    );
    pwButton
      .querySelector("[data-icon='show']")
      ?.toggleAttribute("hidden", reveal);
    pwButton
      .querySelector("[data-icon='hide']")
      ?.toggleAttribute("hidden", !reveal);
  });
</script>

Custom Styling

The passThrough prop provides escape hatches for each slot: style accepts a Panda CSS SystemStyleObject and props forwards arbitrary HTML attributes.

Custom style via passThrough

Custom props via passThrough

---
import Input from "../Input.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Custom style via passThrough</h3>
    <Input
      placeholder="Custom root style"
      passThrough={{
        root: { style: { borderRadius: "full" } },
        input: { style: { fontStyle: "italic" } },
      }}
    />
  </div>

  <div>
    <h3>Custom props via passThrough</h3>
    <Input
      placeholder="Search..."
      passThrough={{
        input: {
          props: {
            "aria-label": "Search input",
            "data-testid": "search-input",
          },
        },
      }}
    />
  </div>
</div>
props · 16 shown · 16 total
InputRoot
background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Background surface variant for the input.

border
"none""bold""default""muted""accent"

Border variant for the input wrapper.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic feedback color scheme used to indicate validation state or context. `neutral` is the default, `primary` uses the primary color, `success` green, `danger` red, and `warning` orange.

fullWidth
boolean

When true, the input stretches to fill the available width of its container.

passThrough
Pick<InputPassThrough<RootElementAttributes>, "root">

Per-slot escape hatch for custom styling and props on the wrapper.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius override. Without it the corner follows the size tier (`control.<size>`). `inner` renders a concentric corner computed from the nearest parent panel (parent radius − parent padding, floored at `xs`).

size
"sm""md""lg""xs"
default "md"

Size variant. All sizes share the same height system as buttons (`xs`, `sm`, `md`, `lg`).

InputField
passThrough
{ root?: InputSlotPassThrough<InputElementAttributes> }

Per-slot escape hatch for custom styling and props on the `<input>`.

type
string
default "text"

HTML input type for the `<input>` element (e.g. `text`, `search`, `number`, `email`, `password`, `tel`, `url`).

Input
background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Background surface variant for the input.

border
"none""bold""default""muted""accent"

Border variant for the input wrapper.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic feedback color scheme used to indicate validation state or context. `neutral` is the default, `primary` uses the primary color, `success` green, `danger` red, and `warning` orange.

fullWidth
boolean

When true, the input stretches to fill the available width of its container.

passThrough
InputPassThrough<RootElementAttributes, InputElementAttributes>

Per-slot escape hatch for custom styling and props. Each slot accepts a Panda CSS `style` object and a `props` bag of arbitrary HTML attributes.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius override. Without it the corner follows the size tier (`control.<size>`). `inner` renders a concentric corner computed from the nearest parent panel (parent radius − parent padding, floored at `xs`).

size
"sm""md""lg""xs"
default "md"

Size variant. All sizes share the same height system as buttons (`xs`, `sm`, `md`, `lg`).

Type

  • Components
  • Blocks