Skip to main content

Input

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

Use Input for native single-line text editing. Compose it with Field for a label and validation message, or InputGroup for adjacent controls.

Basic usage#

import { Field, Input } from 'moraine'

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

Playground#

Props
Slots

Anatomy#

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.

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.

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#

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#

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="[email protected]" />
          </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
data-disabledSlot: inputDescription: Present when the component, slot, or item is disabled.
data-invalidSlot: inputDescription: Present when the field or form has a validation error.
data-readonlySlot: inputDescription: Present when the field is in read-only mode.
data-requiredSlot: inputDescription: Present when the field input is required.

Props#

Props for the Input component. Renders a <input> element by default.

Prop