---
title: Input
description: Edit single-line text with native input events and Field integration.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Input

> Edit single-line text with native input events and Field integration.

Use Input for native single-line text editing. Compose it with [Field](https://moraine.subf.dev/components/field.md) for a label and validation message, or [InputGroup](https://moraine.subf.dev/components/input-group.md) for adjacent controls.

## Basic usage

```tsx
import { Field, Input } from 'moraine'

export function Example() {
  return (
    <Field label="Your name">
      <Input placeholder="Your name" />
    </Field>
  )
}
```

## Anatomy

```text
Input [component; slot=root; <input>]
```

## Usage

### Native input behavior

`Input` forwards native input attributes. Use `value` with `onValueChange` for controlled text and `defaultValue` for an initial uncontrolled value. `readOnly` keeps the field focusable without allowing edits; `disabled` removes it from interaction.

```tsx
import { Field, Input } from 'moraine'
import { createMemo, createSignal, For, Show } from 'solid-js'

const PROJECTS = ['Atlas design system', 'Billing portal', 'Customer dashboard', 'Release tracker']

export function ControlledInput() {
  const [query, setQuery] = createSignal('')
  const matches = createMemo(() =>
    PROJECTS.filter((project) => project.toLowerCase().includes(query().trim().toLowerCase())),
  )

  return (
    <div class="max-w-md w-full space-y-3">
      <Field label="Find a project">
        <Input value={query()} onValueChange={setQuery} placeholder="Search projects..." />
      </Field>
      <Show when={matches().length} fallback={<p class="text-sm">No matching projects.</p>}>
        <ul class="text-sm divide-border divide-y">
          <For each={matches()}>{(project) => <li class="py-2">{project}</li>}</For>
        </ul>
      </Show>
    </div>
  )
}
```

### Composed controls

Use `InputGroup.Leading` and `InputGroup.Trailing` for static context such as an icon or unit. Password visibility, clearing, copying, and loading remain application behavior: compose buttons and icons into these parts and provide their labels and handlers.

```tsx
import { Button, Icon, Input, InputGroup } from 'moraine'
import { createSignal } from 'solid-js'

export function ComposedControls() {
  const [showPassword, setShowPassword] = createSignal(false)

  return (
    <div class="max-w-md w-full space-y-4">
      <InputGroup>
        <InputGroup.Leading>
          <Icon name="i-lucide:key-round" />
        </InputGroup.Leading>
        <Input type={showPassword() ? 'text' : 'password'} placeholder="Enter secret token" />
        <InputGroup.Trailing compact>
          <Button
            variant="ghost"
            size="icon-xs"
            onClick={() => setShowPassword((prev) => !prev)}
            aria-label={showPassword() ? 'Hide token' : 'Show token'}
          >
            <Icon name={showPassword() ? 'i-lucide:eye-off' : 'i-lucide:eye'} />
          </Button>
        </InputGroup.Trailing>
      </InputGroup>
    </div>
  )
}
```

## Examples

### Leading and trailing content

```tsx
import { Button, Icon, Input, InputGroup } from 'moraine'
import { createSignal, Show } from 'solid-js'

export function InputWithIcons() {
  const [showPassword, setShowPassword] = createSignal(false)
  const [query, setQuery] = createSignal('SolidJS reactive components')

  return (
    <div class="flex flex-col gap-4 max-w-sm w-full">
      <InputGroup>
        <InputGroup.Leading>
          <Icon name="i-lucide:lock" />
        </InputGroup.Leading>
        <Input
          type={showPassword() ? 'text' : 'password'}
          defaultValue="my_super_secret_token_123"
          placeholder="Enter password"
        />
        <InputGroup.Trailing compact>
          <Button
            variant="ghost"
            size="icon-sm"
            onClick={() => setShowPassword((prev) => !prev)}
            aria-label={showPassword() ? 'Hide password' : 'Show password'}
          >
            <Icon name={showPassword() ? 'i-lucide:eye-off' : 'i-lucide:eye'} />
          </Button>
        </InputGroup.Trailing>
      </InputGroup>

      <InputGroup>
        <InputGroup.Leading>
          <Icon name="i-lucide:search" />
        </InputGroup.Leading>
        <Input
          value={query()}
          onInput={(e) => setQuery(e.currentTarget.value)}
          placeholder="Search documents..."
        />
        <InputGroup.Trailing compact>
          <Show when={query()}>
            <Button
              variant="ghost"
              size="icon-sm"
              onClick={() => setQuery('')}
              aria-label="Clear search"
            >
              <Icon name="i-lucide:x" />
            </Button>
          </Show>
        </InputGroup.Trailing>
      </InputGroup>

      <InputGroup>
        <InputGroup.Leading compact>
          <div class="text-xs text-muted-foreground font-mono flex gap-1 items-center">
            <Icon name="i-lucide:globe" class="size-3.5" />
            https://
          </div>
        </InputGroup.Leading>
        <Input placeholder="project-slug" />
        <InputGroup.Trailing compact>
          <span class="text-xs text-muted-foreground font-mono">.moraine.dev</span>
        </InputGroup.Trailing>
      </InputGroup>

      <InputGroup>
        <InputGroup.Leading>
          <span class="text-xs text-muted-foreground font-semibold">$</span>
        </InputGroup.Leading>
        <Input defaultValue="49.00" placeholder="0.00" />
        <InputGroup.Trailing>
          <span class="text-xs text-muted-foreground">USD / mo</span>
        </InputGroup.Trailing>
      </InputGroup>
    </div>
  )
}
```

### Form integration

```tsx
import { Button, createForm, Input, InputGroup, Icon } from 'moraine'
import { createSignal } from 'solid-js'
import * as v from 'valibot'

export function FormIntegration() {
  const [submittedEmail, setSubmittedEmail] = createSignal('')
  const form = createForm({
    schema: v.object({
      email: v.pipe(v.string(), v.email('Please enter a valid work email address.')),
    }),
    initialInput: { email: '' },
    validate: 'input',
  })

  return (
    <form.Form onSubmit={(output) => setSubmittedEmail(output.email)}>
      <div class="max-w-xl space-y-4">
        <form.Field
          name="email"
          label="Work Email"
          description="We will send your verification link here."
          required
        >
          <InputGroup>
            <InputGroup.Leading>
              <Icon name="i-lucide:mail" />
            </InputGroup.Leading>
            <Input type="email" placeholder="alex@company.com" />
          </InputGroup>
        </form.Field>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Validate
          </Button>
          <p class="text-xs text-muted-foreground">Submitted: {submittedEmail() || 'none'}</p>
        </div>
      </div>
    </form.Form>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `input` | Present when the component, slot, or item is disabled. |
| `data-invalid` | `input` | Present when the field or form has a validation error. |
| `data-readonly` | `input` | Present when the field is in read-only mode. |
| `data-required` | `input` | Present when the field input is required. |

## Props

Props for the Input component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| autocomplete | JSX.InputHTMLAttributes<HTMLInputElement>['autocomplete'] \| undefined | 'off' | The autocomplete attribute for the input. |
| autofocus | boolean \| undefined | false | Whether the input should automatically receive focus on mount. |
| autofocusDelay | number \| undefined | 0 | The delay in milliseconds before automatically focusing the input. |
| defaultValue | string \| number \| undefined | — | The default value of the input (uncontrolled). |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| id | string \| undefined | — | The ID of the input element. |
| maxLength | number \| string \| undefined | — | The maximum number of characters allowed in the input. |
| modelModifiers | M \| undefined | — | Modifiers for the input value (e.g., trim, lazy, number). |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onBlur | JSX.FocusEventHandlerUnion<HTMLInputElement, FocusEvent> \| undefined | — | Event handler for the blur event. |
| onChange | JSX.EventHandlerUnion<HTMLInputElement, Event> \| undefined | — | Native change event, after value synchronization and Field notification. |
| onFocus | JSX.FocusEventHandlerUnion<HTMLInputElement, FocusEvent> \| undefined | — | Event handler for the focus event. |
| onInput | JSX.InputEventHandlerUnion<HTMLInputElement, InputEvent> \| undefined | — | Event handler for the input event. |
| onValueChange | ((value: ModifierValue<M>) => void) \| undefined | — | Callback when the input value changes during input. |
| placeholder | string \| undefined | — | The placeholder text for the input. |
| readOnly | boolean \| undefined | false | Whether the input is read-only. |
| ref | Ref<HTMLInputElement> \| undefined | — | Ref for the native input element. |
| required | boolean \| undefined | false | Whether the input is required. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| type | JSX.InputHTMLAttributes<HTMLInputElement>['type'] \| undefined | 'text' | The type of the input element. |
| value | string \| number \| undefined | — | The current value of the input (controlled). |
| variant | 'outline' \| 'subtle' \| 'ghost' \| 'none' \| undefined | 'outline' | Visual treatment of the component. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
