component
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.
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>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>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:
---
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>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>The passThrough prop provides escape hatches for each slot: style accepts a Panda CSS SystemStyleObject and props forwards arbitrary HTML attributes.
---
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>Background surface variant for the input.
Border variant for the input wrapper.
"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.
When true, the input stretches to fill the available width of its container.
Per-slot escape hatch for custom styling and props on the wrapper.
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`).
"md"Size variant. All sizes share the same height system as buttons (`xs`, `sm`, `md`, `lg`).
Per-slot escape hatch for custom styling and props on the `<input>`.
"text"HTML input type for the `<input>` element (e.g. `text`, `search`, `number`, `email`, `password`, `tel`, `url`).
Background surface variant for the input.
Border variant for the input wrapper.
"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.
When true, the input stretches to fill the available width of its container.
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.
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`).
"md"Size variant. All sizes share the same height system as buttons (`xs`, `sm`, `md`, `lg`).