component

Input

A text input component with tone 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.

Tone

Apply semantic tone colors to indicate validation state or context.

---
// GENERATED from packages/demos/input/tone.demo.tsx — edit the source, not this file.
import Input from "@pindoba/astro-input";
import { stack } from "@pindoba/styled-system/patterns";
---

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

Size

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

---
// GENERATED from packages/demos/input/size.demo.tsx — edit the source, not this file.
import Input from "@pindoba/astro-input";
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, tone, 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
---
// GENERATED from packages/demos/input/leading-trailing-sizes.demo.tsx — edit the source, not this file.
import { InputRoot, InputField } from "@pindoba/astro-input";
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>

Declarative leading / trailing

The monolith <Input> takes leading and trailing as sugar over <Affix>: it renders the affix boxes inside the frame for you. Style the boxes through passThrough.leading / passThrough.trailing; icons go inside a <Stamp emphasis="ghost">. Reach for the composed <InputRoot> + <Affix> form when an addon needs its own behavior, like the buttons below. Both inputs render identically:

USD
USD
---
// GENERATED from packages/demos/input/declarative-affix.demo.tsx — edit the source, not this file.
import Input from "@pindoba/astro-input";
import { InputRoot, InputField } from "@pindoba/astro-input";
import Affix from "@pindoba/astro-affix";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { DollarSign } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column" })}>
  <Input placeholder="Amount (declarative)">
    <Stamp slot="leading" emphasis="ghost">
      <DollarSign />
    </Stamp>
    <Badge slot="trailing" emphasis="secondary">
      USD
    </Badge>
  </Input>

  <InputRoot>
    <Affix>
      <Stamp emphasis="ghost">
        <DollarSign />
      </Stamp>
    </Affix>
    <InputField placeholder="Amount (imperative)" />
    <Affix side="end">
      <Badge emphasis="secondary">USD</Badge>
    </Affix>
  </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:

---
// GENERATED from packages/demos/input/affix-buttons.demo.tsx — edit the source, not this file.
import { InputRoot, InputField } from "@pindoba/astro-input";
import Affix from "@pindoba/astro-affix";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { Search, X, EyeOff, Eye } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";

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

const search = "Clear me";
const revealed = false;
---

<div
  class={stack({ gap: "md", direction: "column" })}
  data-ir-root="input/affix-buttons"
>
  <InputRoot>
    <Affix decorative>
      <Stamp emphasis="ghost">
        <Search />
      </Stamp>
    </Affix>
    <InputField value="Clear me" placeholder="Search…" data-ir="1" />
    <Fragment set:html="<!--ir:2-->" />
    {search && (
      <Affix side="end">
        <Button emphasis="ghost" shape="circle" aria-label="Clear" data-ir="3">
          <X />
        </Button>
      </Affix>
    )}
    <Fragment set:html="<!--/ir-->" />
  </InputRoot>

  <InputRoot>
    <InputField
      value="hunter2"
      type={revealed ? "text" : "password"}
      placeholder="Password"
      data-ir="4"
    />
    <Affix side="end">
      <Button
        emphasis="ghost"
        shape="square"
        aria-label={revealed ? "Hide password" : "Show password"}
        aria-pressed={revealed ? "true" : "false"}
        data-ir="5"
      >
        <Fragment set:html="<!--ir:6-->" />
        {revealed ? (
          <>
            <EyeOff />
          </>
        ) : (
          <>
            <Eye />
          </>
        )}
        <Fragment set:html="<!--/ir-->" />
      </Button>
    </Affix>
  </InputRoot>

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

<script>
  import { hydrate } from "./_client/affix-buttons.ts";

  document
    .querySelectorAll('[data-ir-root="input/affix-buttons"]')
    .forEach((root) => hydrate([root]));
</script>

Focusing the field (React ref)

In React, ref on <Input> points at the native <input>, so you can focus or select the field without switching to the parts. Input is a forwardRef component, so this works on React 18 and React 19:

import { useRef } from "react";
import { Input } from "@pindoba/react-input";

export function Example() {
  const ref = useRef<HTMLInputElement>(null);
  return (
    <>
      <Input ref={ref} placeholder="Name" />
      <button onClick={() => ref.current?.focus()}>Focus</button>
    </>
  );
}

InputField takes a ref to the same element when you compose the parts.

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

---
// GENERATED from packages/demos/input/custom.demo.tsx — edit the source, not this file.
import Input from "@pindoba/astro-input";
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>
  • Components
  • Blocks