Search Field
Free · MITHeadless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.
Install
Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).
Unstyled — behavior only, you write the CSS
npx astrocraft-ui add forms/search-fieldPlain-CSS theme — no build step
npx astrocraft-ui add forms/search-field --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add forms/search-field --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add forms/search-field --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add forms/search-field --theme tailwind --bridge lumosLive demo
InputGroup & SearchField
An addon takes up width; an element sits on top of the field. The search field’s clear button appears once there is something to clear, and Escape empties it without leaving it.
Attribute surface
With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.
| Component | Slots | Variants | Runtime state | Native state |
|---|---|---|---|---|
| `SearchField.astro` | `search-field` `search-field-clear` `search-field-input` | `data-size`: `sm` · `md` · `lg` `data-state`: `default` · `error` · `success` | — | `[hidden]` |
Source
What the command copies — 2 files, alias-free so the copy lands standing alone. This is the whole component; there is nothing else.
---
// src/components/ui/forms/search-field/SearchField.astro — headless primitive (see ../../README.md).
// A native <input type="search"> with a clear button you can actually style. `type=search` already
// carries the searchbox role and the mobile "Search" key, so none of that is re-declared here.
//
// What the script adds is the clear affordance, because the UA's own is unreachable: it is a
// `::-webkit-search-cancel-button` pseudo-element with no styling surface, it exists in exactly one
// engine, and it is mouse-only. structure.css suppresses it so there is never a second, unstyleable
// X beside ours — the same reasoning as the Accordion's duplicate disclosure marker.
//
// The button is hidden with the `hidden` ATTRIBUTE while the field is empty (never a class — see
// contract rule 4), and Escape clears the field without leaving it, which is what the pattern is for.
// With JavaScript off it is a plain, fully-usable search field with no clear button.
import "../../../../styles/structure.css";
import type { HTMLAttributes } from "astro/types";
type Props = HTMLAttributes<"input"> & {
size?: "sm" | "md" | "lg";
state?: "default" | "error" | "success";
clearLabel?: string;
};
const {
size = "md",
state = "default",
clearLabel = "Clear search",
class: className,
...rest
} = Astro.props;
---
<div data-slot="search-field" data-size={size}>
<input
type="search"
class={className}
data-slot="search-field-input"
data-size={size}
data-state={state}
{...rest}
/>
<button type="button" data-slot="search-field-clear" aria-label={clearLabel} hidden>
<slot name="clear"
><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18M6 6l12 12"></path>
</svg></slot
>
</button>
</div>
<script>
import { onReadyOnce } from "../../_once";
function wire(root: HTMLElement) {
const field = root.querySelector<HTMLInputElement>('[data-slot="search-field-input"]');
const clear = root.querySelector<HTMLButtonElement>('[data-slot="search-field-clear"]');
if (!field || !clear) return;
const sync = () => {
clear.hidden = field.value === "";
};
const reset = () => {
if (field.value === "") return;
field.value = "";
// Consumers listen for `input` to re-filter and `change` to commit; clearing is both.
field.dispatchEvent(new Event("input", { bubbles: true }));
field.dispatchEvent(new Event("change", { bubbles: true }));
sync();
};
field.addEventListener("input", sync);
field.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
// Stop here: an Escape inside a dialog or a popover would otherwise close it out from under
// someone who only meant to empty the field.
event.stopPropagation();
reset();
});
clear.addEventListener("click", () => {
reset();
field.focus(); // the button vanishes on clear, so focus has to go somewhere deliberate
});
sync();
}
onReadyOnce('[data-slot="search-field"]', wire);
</script>
import SearchField from "./SearchField.astro";
export { SearchField };
export default SearchField;
What you get
The component source, copied into your project by npx astrocraft-ui add forms/search-field — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.