Morphos
Inputs

Field

A compound component that provides accessible label, description, and error wiring for any form control.

Interactive example

Installation

npm install @morphos/inputs @morphos/core
pnpm add @morphos/inputs @morphos/core
yarn add @morphos/inputs @morphos/core
bun add @morphos/inputs @morphos/core

Import

import { Field, FieldLabel, FieldDescription, FieldError, FieldControl } from '@morphos/inputs'

Compound components

ComponentDescription
FieldRoot that manages shared IDs and invalid/disabled/required state, rendered as a <div>
FieldLabelRenders a <label> linked to the control via htmlFor={field.fieldId}
FieldDescriptionRenders a <p> with a stable ID for aria-describedby
FieldErrorRenders a <p role="alert"> only when field.invalid is true
FieldControlWrapper <div> for the input element

Usage

@Component()
class MyComponent extends StatefulComponent {
  @State() field = new Field({ required: true })
  @State() email = ''
  @State() invalid = false

  validate() {
    this.invalid = !this.email.includes('@')
  }

  render() {
    return (
      <Field class="morphos-field" required invalid={this.invalid}>
        <FieldLabel field={this.field} class="morphos-field-label">Email address</FieldLabel>
        <FieldDescription field={this.field} class="morphos-field-description">
          We will never share your email with anyone.
        </FieldDescription>
        <FieldControl class="morphos-field-control">
          <Input
            id={this.field.fieldId}
            type="email"
            class="morphos-input"
            value={this.email}
            onInput={(value) => (this.email = value)}
            onBlur={() => this.validate()}
            aria-describedby={this.field.descriptionId}
            invalid={this.invalid}
          />
        </FieldControl>
        <FieldError field={this.field} class="morphos-field-error">
          Please enter a valid email address.
        </FieldError>
      </Field>
    )
  }
}

Field is a regular stateful component rendered directly with props (required, invalid, disabled, class, ...). The separate new Field(...) instance kept in @State() is what FieldLabel, FieldDescription, and FieldError read their field.fieldId / field.descriptionId / field.errorId / field.invalid from — construct it with matching options so the IDs and state line up with what you pass to the rendered <Field>.

Disabled field

@Component()
class MyComponent extends StatefulComponent {
  @State() field = new Field({ disabled: true })

  render() {
    return (
      <Field class="morphos-field" disabled>
        <FieldLabel field={this.field} class="morphos-field-label">Account type</FieldLabel>
        <FieldControl class="morphos-field-control">
          <Input id={this.field.fieldId} class="morphos-input" value="Enterprise" disabled />
        </FieldControl>
        <FieldDescription field={this.field} class="morphos-field-description">
          Contact support to change your account type.
        </FieldDescription>
      </Field>
    )
  }
}

With Select

@Component()
class MyComponent extends StatefulComponent {
  @State() field = new Field({ required: true })

  render() {
    return (
      <Field class="morphos-field" required>
        <FieldLabel field={this.field} class="morphos-field-label">Language</FieldLabel>
        <FieldControl class="morphos-field-control">
          <Select
            id={this.field.fieldId}
            class="morphos-select"
            options={[
              { value: 'en', label: 'English' },
              { value: 'es', label: 'Spanish' },
              { value: 'pt', label: 'Portuguese' },
            ]}
            placeholder="Choose language"
          />
        </FieldControl>
      </Field>
    )
  }
}

Props — Field

PropTypeDefaultDescription
invalidbooleanfalseMarks the field as invalid; sets data-invalid and is read by FieldError to decide whether to render
disabledbooleanfalseMarks the field as disabled
requiredbooleanfalseMarks the field as required
classstringAdditional CSS classes
idstringHTML id attribute; also used as fieldId when set
childrenChildrenThe label, description, control, and error parts

Field instance properties

PropertyTypeDescription
fieldIdstringThe id prop if provided, otherwise an auto-generated ID — intended for the primary input element
descriptionIdstringAuto-generated ID for FieldDescription
errorIdstringAuto-generated ID for FieldError

Props — FieldLabel

PropTypeDefaultDescription
fieldFieldThe parent Field instance (required)
childrenChildrenLabel text
classstringAdditional CSS classes
idstringHTML id attribute on the <label> itself

Props — FieldDescription

PropTypeDefaultDescription
fieldFieldThe parent Field instance (required)
childrenChildrenDescription text
classstringAdditional CSS classes
idstringHTML id attribute; overrides the auto-generated descriptionId

Props — FieldError

PropTypeDefaultDescription
fieldFieldThe parent Field instance (required)
childrenChildrenError message text
classstringAdditional CSS classes
idstringHTML id attribute; overrides the auto-generated errorId

Props — FieldControl

PropTypeDefaultDescription
childrenChildrenThe form input element(s)
classstringAdditional CSS classes

data-* attributes

AttributeElementWhen present
data-invalidField rootinvalid is true
data-disabledField rootdisabled is true
data-requiredField rootrequired is true

Accessibility

Field auto-generates stable IDs for the description and error elements (descriptionId, errorId) and resolves fieldId from its own id prop or an auto-generated fallback. FieldLabel renders <label htmlFor={field.fieldId}>, so give your input id={field.fieldId} for the label to focus it on click. FieldError uses role="alert" and is only rendered when field.invalid is true, so the error is announced to screen readers as soon as it appears.

You do not need to use FieldControl if you do not need a wrapper div — it is provided as a styling convenience only, and its field prop (declared in the types) is not read by its render().

Field, FieldDescription, and FieldError do not expose a labelId. Wire aria-describedby={field.descriptionId} and the invalid/required props manually onto your input — there is no aria-errormessage prop on @morphos/inputs form controls.

Styling example

.morphos-field[data-required] .morphos-field-label::after {
  content: " *";
  color: var(--morphos-color-danger);
}

.morphos-field-error {
  color: var(--morphos-color-danger);
}

.morphos-field[data-disabled] {
  opacity: 0.5;
}

On this page