Input
One line of text: a name, an email, a URL.
Preview
tsx
import { Grid, Input } from '@kerf-js/veneer'export function Variants() { return ( <Grid cols={3} gap="group" > <Input label="Project name" defaultValue="storefront" /> <Input label="Project name" defaultValue="storefront" variant="classic" /> <Input label="Project name" defaultValue="storefront" appearance="line" /> </Grid> )}Installation
npm install @kerf-js/veneerTry it
Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.
Live preview
Controls
Size
Variant
Codeupdates as you change the controls
import { Input } from '@kerf-js/veneer'<Input label="Project name" hint="Lowercase letters and dashes." clearable />Usage
tsx
import { Input } from '@kerf-js/veneer'Sizes
Use size to pick sm, md or lg.
tsx
import { Input, Stack } from '@kerf-js/veneer'export function Sizes() { return ( <Stack gap="group"> <Input label="Project name" size="sm" /> <Input label="Project name" /> <Input label="Project name" size="lg" /> </Stack> )}With icon
Every native attribute passes through: type, autocomplete, inputmode.
tsx
import { Input, Stack } from '@kerf-js/veneer'export function Types() { return ( <Stack gap="group"> <Input label="Work email" type="email" autocomplete="email" icon="mail" /> <Input label="Password" type="password" autocomplete="new-password" icon="lock" /> <Input label="Repository" type="url" placeholder="https://github.com/kerf/storefront" icon="link" /> </Stack> )}Prefix and suffix
Use prefix and suffix to show fixed text before and after the value.
tsx
import { Input, Stack } from '@kerf-js/veneer'export function Affixes() { return ( <Stack gap="group"> <Input label="Domain" prefix="https://" suffix=".kerf.dev" defaultValue="storefront" /> <Input label="Monthly budget" prefix="€" suffix="per month" inputmode="numeric" defaultValue="1,240" /> </Stack> )}States
An input can show a hint or an error, and can be disabled or readonly.
tsx
import { Grid, Input } from '@kerf-js/veneer'import { Stack, Text } from '@kerf-js/veneer'export function States() { return ( <Grid cols={3} gap="group" > <Cell name="Empty"> <Input label="Project name" /> </Cell> <Cell name="Filled"> <Input label="Project name" defaultValue="storefront" /> </Cell> <Cell name="Hint"> <Input label="Project name" hint="Lowercase letters and dashes." /> </Cell> <Cell name="Invalid"> <Input label="Project name" defaultValue="Store Front" error="Use lowercase letters and dashes." /> </Cell> <Cell name="Disabled"> <Input label="Project name" defaultValue="storefront" disabled /> </Cell> <Cell name="Read-only"> <Input label="Project name" defaultValue="storefront" readonly /> </Cell> </Grid> )}const Cell = ({ name, children,}: { name: string children?: unknown}) => ( <Stack gap="tight"> <Text role="caption" subtle > {name} </Text> {children} </Stack>)Validation
Clear the field to see the error.
tsx
import { derived, state } from '@kerf-js/core'import { Input, Stack, Text } from '@kerf-js/veneer'export function Slug() { const name = state('Store Front') const slug = derived(() => name.value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''), ) return ( <Stack gap="related"> <Input label="Project name" value={name.value} onInput={(e) => (name.value = e.currentTarget.value) } error={ slug.value ? undefined : 'Give it a name with at least one letter.' } /> <Text subtle> {slug.value ? `Lives at ${slug.value}.kerf.dev` : 'Waiting for a name'} </Text> </Stack> )}Project settings form
Fields share one form, and an input's error shows when its value is not valid.
tsx
import { derived, state } from '@kerf-js/core'import { Button, Checkbox, Grid, Input, Row, Select, Slider, Stack, Text, Textarea, Toaster, toast} from '@kerf-js/veneer'export function FormProjectSettings() { const name = state('storefront') const about = state(initialAbout) const region = state<string | null>('eu-west') const copies = state(3) const onPush = state(true) const problem = derived(() => nameProblem(name.value)) const save = (e: Event) => { e.preventDefault() if (!problem.value) toast.show({ title: 'Settings saved', description: `${name.value} runs ${copies.value} copies ` + `in ${region.value}.`, }) } return ( <> <Stack as="form" gap="group" onSubmit={save} > <Text role="heading">Project settings</Text> <Grid cols={2} gap="group" > <Input label="Project name" value={name.value} error={problem.value} onInput={(e) => (name.value = e.currentTarget.value) } /> <Select label="Region" options={REGION_OPTIONS} value={region} hint="Moving a project redeploys it." /> </Grid> <Textarea label="Description" value={about.value} counter={120} onInput={(e) => (about.value = e.currentTarget.value) } /> <Slider label="Copies" value={copies.value} min={1} max={8} onChange={(v) => (copies.value = v)} /> <Checkbox checked={onPush.value} hint="Every push to main goes live." onChange={( e: Event & { currentTarget: HTMLInputElement }, ) => (onPush.value = e.currentTarget.checked)} > Deploy on push </Checkbox> <FormActions /> </Stack> <Toaster /> </> )}const REGIONS = [ { region: 'eu-west', requests: 34.2, share: 40 }, { region: 'us-east', requests: 24.8, share: 29 }, { region: 'ap-south', requests: 12.1, share: 14 }, { region: 'us-west', requests: 9.4, share: 11 }, { region: 'eu-north', requests: 5.6, share: 6 },]const min = 60_000const SLUG = /^[a-z0-9-]+$/const REGION_OPTIONS = REGIONS.map((r) => ({ value: r.region, label: r.region,}))const initialAbout = 'The public shop, served from the edge.'function nameProblem(name: string) { return SLUG.test(name) ? undefined : 'Use lowercase letters, numbers and dashes.'}function FormActions() { return ( <Row gap="related" justify="end" > <Button emphasis="ghost" tone="neutral" > Cancel </Button> <Button type="submit">Save changes</Button> </Row> )}When to use
Accessibility
- Tab
- Moves in and out
- Esc
- Clears a `clearable` field
- The label is a real
<label>, and the hint and error are tied to the input witharia-describedby. - An error is announced when it appears.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | The name people read and screen readers announce. Always required, and it floats on the border by default. |
hint | unknown | — | A short line of help under the field. |
error | unknown | — | Replaces the hint and marks the field invalid. |
icon | IconRef | — | An icon before the text. |
prefix | unknown | — | Fixed text before the value, like https://. |
suffix | unknown | — | Fixed text after the value, like a unit. |
variant | "floating" | "classic" | — | floating puts the label on the border, classic above the field. |
appearance | "line" | — | line draws only the bottom edge. |
size | "sm" | "md" | "lg" | — | sm, md or lg. Each follows the density around it. |
clearable | boolean | false | A clear button, shown once there is a value. |
counter | number | — | The most it should hold. Draws a count under the field. |
id | string | — | The element id. One is made for you if you leave it out. |
value | string | — | The current value, when you own the state. |
disabled | boolean | false | Can't be focused or changed, and doesn't post with a form. |
readonly | boolean | false | Can be focused, read and copied, but not changed. It still posts. |
placeholder | string | — | Sample text shown while empty. Never a substitute for a label. |
type | string | — | The native input type, such as email, url or password. Text otherwise. |
| Event | Type | Default | Description |
|---|---|---|---|
onInput | ((e: InputEvent & { currentTarget: HTMLInputElement & HTMLTextAreaElemen… | — | Called on each keystroke. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="input"] [data-part="input"]
input