Input
A text field.
import { Div, signal, Span } from "@implementjs/core";
import { Input } from "@/lib/components/ui/input";
import { Label } from "@/lib/components/ui/label";
export default function InputDemo() {
const value = signal("");
return Div(
{ class: "flex w-full max-w-sm flex-col gap-4" },
Div(
{ class: "flex flex-col gap-2" },
Label({ for: "project" }, "Project name"),
Input({ id: "project", placeholder: "acme-web", value }),
Span(
{ class: "text-sm text-muted-foreground" },
value.bind((current) => (current === "" ? "Nothing typed yet." : `Typed: ${current}`)),
),
),
Input({ placeholder: "Disabled", disabled: true }),
Input({ placeholder: "Invalid", "aria-invalid": true }),
);
}Installation
npx jsrepo add @implementjs/ui/input
Nothing else comes with it — this one stands alone on @implementjs/core and @implementjs/primitives.
Copy the file below to src/lib/components/ui/input.ts. It imports cn, so copy utils.ts to src/lib/utils.ts too.
import { Input as InputElement, type ElementProps } from "@implementjs/core";
import { createComponent } from "@implementjs/primitives";
import { cn } from "@/lib/utils";
export type InputProps = ElementProps<"input">;
/**
* A text field. Every prop goes through to the `input`, so `type`, `value`,
* `placeholder`, and the rest work as they always do — this only dresses it.
*
* `aria-invalid` is styled as well as announced, which is how `FieldError`
* marks the control it belongs to.
*/
export const Input = createComponent(function Input({ class: className, ...props }: InputProps) {
return InputElement({
...props,
"data-slot": "input",
class: cn(
"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm",
"placeholder:text-muted-foreground",
"selection:bg-primary selection:text-primary-foreground",
"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
"disabled:cursor-not-allowed disabled:opacity-50",
"dark:bg-input/30",
className,
),
});
});Usage
import { Input } from "@/lib/components/ui/input";
Input({ id: "email", type: "email", placeholder: "you@example.com" });
Every prop goes through to the input, so type, value, placeholder, required, and the rest work as they always do — this only dresses it.
Binding a value
Pass a signal as value to read what is typed:
const email = signal("");
Input({ id: "email", type: "email", value: email });
Labels and errors
Pair it with a label by for and id, or let field arrange the label, the hint, and the error together.
aria-invalid is styled as well as announced — the border and focus ring turn destructive — which is how FieldError marks the control it belongs to.
File inputs
type="file" is styled too: the file: variants give the button the same text and weight as the rest of the field, instead of the browser default.
API Reference
Input
A text field. Every prop goes through, so type, value, and placeholder work as they always do. aria-invalid is styled as well as announced, which is how FieldError marks its control. Renders a Input; extra props are forwarded onto it.