Morphos
Inputs

Form

A lightweight form wrapper component with typed event props for submit and reset handling.

Interactive example

Open in Storybook

Installation

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

Import

import { Form } from '@morphos/inputs'

Usage

@Component()
class MyComponent extends StatefulComponent {
  @State() name = ''
  @State() email = ''
  @State() nameField = new Field()
  @State() emailField = new Field()

  handleSubmit(e: SubmitEvent) {
    e.preventDefault()
    const data = new FormData(e.target as HTMLFormElement)
    console.log({
      name: data.get('name'),
      email: data.get('email'),
    })
  }

  render() {
    return (
      <Form class="morphos-form" onSubmit={(e) => this.handleSubmit(e)}>
        <Field class="morphos-field">
          <FieldLabel field={this.nameField} class="morphos-field-label">Name</FieldLabel>
          <FieldControl class="morphos-field-control">
            <Input
              id={this.nameField.fieldId}
              class="morphos-input"
              name="name"
              value={this.name}
              onInput={(value) => (this.name = value)}
            />
          </FieldControl>
        </Field>
        <Field class="morphos-field">
          <FieldLabel field={this.emailField} class="morphos-field-label">Email</FieldLabel>
          <FieldControl class="morphos-field-control">
            <Input
              id={this.emailField.fieldId}
              class="morphos-input"
              type="email"
              name="email"
              value={this.email}
              onInput={(value) => (this.email = value)}
            />
          </FieldControl>
        </Field>
        <button type="submit" class="morphos-button">Submit</button>
      </Form>
    )
  }
}

With noValidate

@Component()
class MyComponent extends StatefulComponent {
  @State() errors: Record<string, string> = {}

  validate(data: FormData): boolean {
    const errs: Record<string, string> = {}
    if (!data.get('username')) errs.username = 'Username is required'
    if ((data.get('password') as string).length < 8)
      errs.password = 'Password must be at least 8 characters'
    this.errors = errs
    return Object.keys(errs).length === 0
  }

  handleSubmit(e: SubmitEvent) {
    e.preventDefault()
    const data = new FormData(e.target as HTMLFormElement)
    if (this.validate(data)) {
      // proceed with submission
    }
  }

  render() {
    return (
      <Form noValidate onSubmit={(e) => this.handleSubmit(e)}>
        <Input name="username" placeholder="Username" aria-label="Username" />
        {this.errors.username && <span role="alert">{this.errors.username}</span>}
        <Input type="password" name="password" placeholder="Password" aria-label="Password" />
        {this.errors.password && <span role="alert">{this.errors.password}</span>}
        <button type="submit">Log in</button>
      </Form>
    )
  }
}

Server action form

@Component()
class MyComponent extends StatefulComponent {
  handleReset() {
    console.log('Form was reset')
  }

  render() {
    return (
      <Form action="/api/contact" method="post" onReset={() => this.handleReset()}>
        <Input name="subject" placeholder="Subject" aria-label="Subject" />
        <Input name="message" placeholder="Message" aria-label="Message" />
        <button type="submit">Send</button>
        <button type="reset">Clear</button>
      </Form>
    )
  }
}

Props — Form

PropTypeDefaultDescription
actionstring—URL to submit the form to
method"get" | "post"—HTTP method for submission
noValidateboolean—Disables native browser validation UI
onSubmit(event: SubmitEvent) => void—Fires when the form is submitted
onReset(event: Event) => void—Fires when the form is reset
childrenChildren—Form fields and controls
classstring—Additional CSS classes
idstring—HTML id attribute
aria-labelstring—Accessible label for the form landmark
aria-labelledbystring—ID of an element that labels the form landmark
aria-describedbystring—ID of an element that describes the form landmark

Accessibility

Form renders a native <form> element, which is automatically recognized as a form landmark by assistive technologies. When aria-label is provided, screen readers announce the label when users navigate to the form landmark. Use noValidate when implementing custom validation logic with Field and FieldError components to avoid conflicting browser-native error popups.

Pair Form with Field, FieldLabel, FieldDescription, and FieldError for fully accessible form fields. Field auto-generates the descriptionId/errorId/fieldId needed to connect labels, descriptions, and error messages to their respective inputs.

Styling example

.morphos-form {
  display: flex;
  flex-direction: column;
  gap: var(--morphos-space-4);
}

On this page