Skip to content
kerfjsGitHub

Input

One line of text: a name, an email, a URL.

SourcePackage @kerf-js/veneerTry it

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/veneer

Try 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>  )}

Clear button and counter

clearable shows once there is text. counter is the most it should hold.

tsx
import { Input, Stack } from '@kerf-js/veneer'export function ClearableCounter() {  return (  <Stack gap="group">    <Input      label="Branch"      clearable      defaultValue="ranking-v2"    />    <Input      label="Commit message"      counter={72}      defaultValue="Rank exact matches first"    />  </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

Use it for

  • A short answer people type
  • Any native input type: email, password, url, tel

Reach for something else

  • Textarea, when the answer can run over one line
  • Select, when the answer comes from a short list
  • Search, when it filters or finds things

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 with aria-describedby.
  • An error is announced when it appears.

API Reference

PropTypeDefaultDescription
labelstring—The name people read and screen readers announce. Always required, and it floats on the border by default.
hintunknown—A short line of help under the field.
errorunknown—Replaces the hint and marks the field invalid.
iconIconRef—An icon before the text.
prefixunknown—Fixed text before the value, like https://.
suffixunknown—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.
clearablebooleanfalseA clear button, shown once there is a value.
counternumber—The most it should hold. Draws a count under the field.
idstring—The element id. One is made for you if you leave it out.
valuestring—The current value, when you own the state.
disabledbooleanfalseCan't be focused or changed, and doesn't post with a form.
readonlybooleanfalseCan be focused, read and copied, but not changed. It still posts.
placeholderstring—Sample text shown while empty. Never a substitute for a label.
typestring—The native input type, such as email, url or password. Text otherwise.
EventTypeDefaultDescription
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

RelatedFieldTextareaSearchSelect