# Input

A native input that joins a Field on its own, with the ID, the ARIA wiring and the states already right.

Input renders a real `<input>` and joins the surrounding
[`Field`](/components/field) without being asked. Use it for a single line of
text; for anything longer, use [`Textarea`](/components/textarea).

::component-preview{name="InputPreview"}
```vue
<template>
  <div class="flex w-[17rem] flex-col gap-6">
    <Field.Root
      class="group flex flex-col gap-1.5"
      :options="{ name: 'email', validate: validateEmail }"
    >
      <div :class="box">
        <div :class="stack">
          <span :class="line">
            <Input
              v-model="email"
              :class="control"
              type="email"
              placeholder=" "
              autocomplete="off"
            />
          </span>
          <Label :class="label">Email</Label>
        </div>
      </div>

      <Field.Description :class="hint">
        We only use this to send receipts.
      </Field.Description>

      <Field.Error v-slot="{ messages }" :class="error">
        {{ messages.join(' ') }}
      </Field.Error>
    </Field.Root>

    <Field.Root
      class="group flex flex-col gap-1.5"
      :options="{ name: 'account', disabled: true }"
    >
      <div :class="box">
        <div :class="stack">
          <span :class="line">
            <Input
              :class="control"
              default-value="team@example.com"
              placeholder=" "
            />
          </span>
          <Label :class="label">Account</Label>
        </div>
      </div>
    </Field.Root>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Field, Input, Label } from '@maas/mirror/vue'

const email = ref('')

const box = [
  'relative isolate flex h-12 w-full cursor-text items-center justify-between gap-1.5',
  'rounded-component-lg border-surface border-2 px-[0.875rem]',
  'transition-all duration-100 ease-linear [&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
  'outline-4 outline-transparent focus-within:focus-ring',
  'group-[[data-invalid=true]:not([data-focused=true])]:border-danger-subtle',
  'group-[[data-invalid=true]:not([data-focused=true])]:bg-danger-subtle',
  'group-data-[disabled=true]:border-disabled-subtle group-data-[disabled=true]:cursor-not-allowed',
].join(' ')

const stack = [
  'group/stack flex h-full max-h-full w-full flex-col-reverse',
  'items-center justify-center gap-0 px-1',
  'focus-within:gap-1 has-[input:not(:placeholder-shown)]:gap-1',
].join(' ')

const line = [
  'relative flex h-0 w-full items-end',
  'group-focus-within/stack:h-[0.9375rem]',
  'has-[input:not(:placeholder-shown)]:h-[0.9375rem]',
].join(' ')

const control = [
  'type-component-lg leading-[normal]! text-surface',
  'block box-content h-[1lh] w-full py-1.25 -my-1.25 appearance-none bg-transparent outline-none',
  'placeholder:text-transparent',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-on-muted',
  'data-[disabled=true]:text-disabled-solid',
].join(' ')

const label = [
  'type-component-lg leading-[normal]! text-surface-muted',
  'flex h-full w-full items-center [--mirror-label-cursor:text]',
  "before:absolute before:-inset-0.5 before:-z-10 before:content-['']",
  'group-focus-within/stack:h-auto group-focus-within/stack:text-[0.6875rem] group-focus-within/stack:[--mirror-label-cursor:auto]',
  'group-has-[input:not(:placeholder-shown)]/stack:h-auto group-has-[input:not(:placeholder-shown)]/stack:text-[0.6875rem] group-has-[input:not(:placeholder-shown)]/stack:[--mirror-label-cursor:auto]',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-muted',
  'group-data-[disabled=true]:text-disabled-muted',
].join(' ')

const hint = 'type-component-2xs text-surface-muted'
const error = 'type-component-2xs text-danger-muted'

function validateEmail(value: unknown) {
  const entry = String(value ?? '')

  if (entry.length === 0) {
    return 'Enter an email address.'
  }

  return entry.includes('@') ? null : 'Enter a valid email address.'
}
</script>
```
::

## Usage guidelines

- Set `id` on `Field.Root` rather than on the input, because `Label` points its
  `for` at the field’s control ID and an `id` here leaves it pointing at
  nothing.
- Only one control may register per field, so two inputs need two fields; a
  second one throws `duplicate_field_control`.

## Anatomy

Render it inside a `Field.Root`, or on its own.

::component-anatomy
---
parts:
  - name: Input
    required: true
    description: Renders a native <input>, joined to the surrounding Field.
---
::

```vue
<script setup lang="ts">
import { Field, Input, Label } from '@maas/mirror/vue'
</script>

<template>
  <Input v-model="query" type="search" />

  <Field.Root :options="{ name: 'email' }">
    <Label>Email</Label>
    <Input v-model="email" type="email" />
  </Field.Root>
</template>
```

## Examples

### Controlled and uncontrolled

`defaultValue` seeds the value and leaves `Input` owning it, while `v-model`
hands it to you, and which of the two applies is
[decided once at mount](/components/composition#controlled-and-uncontrolled).

```vue
<template>
  <Input default-value="Robin" name="nickname" @update:model-value="track" />

  <Input v-model="query" type="search" placeholder="Search" />
</template>

<script setup lang="ts">
const query = ref('')
</script>
```

Start a controlled input from `''` rather than `undefined`, or it stays
uncontrolled for as long as it is mounted.

### Inside a field

The input takes the field’s control ID, `name` and flags, and reports its value,
its focus and its blur back, which is what the field derives `data-dirty`,
`data-touched` and `data-focused` from.

```vue
<template>
  <Field.Root
    :options="{ name: 'email', required: true, validationMode: 'onBlur' }"
  >
    <Label>Email</Label>
    <Input v-model="email" type="email" />
    <Field.Description>We only use this to send receipts.</Field.Description>
    <Field.Error match="valueMissing">Enter an email address.</Field.Error>
    <Field.Error match="typeMismatch">That is not an email address.</Field.Error>
  </Field.Root>
</template>
```

Because the element is a real `<input>`, the field reads its `ValidityState`
directly, which is what lets `Field.Error` match on `valueMissing` and its
siblings.

Each inherited flag resolves as the prop, then the field’s value, then `false`,
so leaving a prop off entirely is how you inherit it.

```vue
<template>
  <Field.Root :options="{ name: 'slug', disabled: true }">
    <Label>Slug</Label>
    <Input v-model="slug" :disabled="false" />
  </Field.Root>
</template>
```

### Outside a field

Without a field, `Input` is a bare `<input>` with a generated ID and only the
states it can work out for itself, so `data-valid`, `data-invalid`, `data-dirty`
and `data-touched` are absent rather than guessed.

```vue
<template>
  <Input v-model="query" type="search" required />
</template>
```

### Styling from state

Every state is an attribute selector away, with no bound class in between.

```vue
<template>
  <Input v-model="email" class="input" type="email" />
</template>

<style>
.input[data-focused='true'] {
  outline: 2px solid var(--app-color-focus-outline);
}

.input[data-invalid='true'] {
  border-color: var(--app-color-danger-border-subtle);
}
</style>
```

### Focusing the input

`Input` renders a void element, so it exposes `focus()`, `blur()` and `select()`
on its component instance instead of a slot.

```vue
<template>
  <Input ref="coupon" default-value="SUMMER" name="coupon" />
  <Button @click="coupon?.select()">Replace code</Button>
</template>

<script setup lang="ts">
const coupon = useTemplateRef('coupon')
</script>
```

## API reference

**Standalone.** Ordinary props, no store. Attributes not listed below
(`placeholder`, `autocomplete`, `inputmode`, `min`, `max`, `pattern`) fall
through to the element unchanged.

### `Input`

Renders an `<input>`.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The element’s DOM ID.
      - label: string
      - label: the field’s `controlId`, else generated
        plaintext: true
  - items:
      - label: modelValue
        description: The value. `v-model`.
      - label: 'string | number'
      - label: undefined
  - items:
      - label: defaultValue
        description: Initial value while uncontrolled.
      - label: 'string | number'
      - label: ''''''
  - items:
      - label: type
        description: 'Passed to the element unchanged. `checkbox` and `radio` throw.'
      - label: string
      - label: '''text'''
  - items:
      - label: name
        description: Form field name. Falls back to the field’s `name`.
      - label: string
      - label: undefined
  - items:
      - label: disabled
        description: Falls back to the field’s `disabled`, then `false`.
      - label: boolean
      - label: undefined
  - items:
      - label: readOnly
        description: Falls back to the field’s `readOnly`, then `false`.
      - label: boolean
      - label: undefined
  - items:
      - label: required
        description: Falls back to the field’s `required`, then `false`.
      - label: boolean
      - label: undefined
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: On every `input` event, controlled or not.
      - label: 'string | number'
  - items:
      - label: focus
        description: Focus enters the input.
      - label: FocusEvent
  - items:
      - label: blur
        description: Focus leaves the input.
      - label: FocusEvent
---
::

#### Slot props

None. `Input` renders a void element and exposes `focus()`, `blur()` and
`select()` on its instance instead.

#### Data attributes

The [field state set](/components/styling#the-field-state-set), with no additions and no
`data-scope`.

::data-attributes
::

#### CSS variables

::css-variables
::

#### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: invalid_input_type
        description: '`type` is `checkbox` or `radio`, at mount or later. [`Checkbox`](/components/checkbox) and [`Radio`](/components/radio) own those two.'
  - items:
      - label: duplicate_field_control
        description: A second control registers into the same `Field.Root`. Thrown by [`Field.Root`](/components/field), not by `Input`.
---
::

## Accessibility

Native throughout: no roles, no key handlers, and inside a `Field` nothing but
`aria-describedby`, `aria-invalid`, `aria-required` and `aria-readonly` is
added. `disabled` renders the native attribute, so the input leaves the tab
order the way any disabled input does.
