Skip to main content
Get started with TanStack Form in Lit by creating a form controller and connecting it to your form fields.

Installation

Install the Lit adapter for TanStack Form:
npm install @tanstack/lit-form

Your First Form

The bare minimum to get started with TanStack Form is to create a TanStackFormController instance in your Lit component.
1
Define Your Form Data Type
2
Start by defining an interface for your form data:
3
interface Employee {
  firstName: string
  lastName: string
}
4
Create the Form Controller
5
In your Lit component, create a private form controller with default values:
6
import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'

@customElement('my-form')
export class MyForm extends LitElement {
  #form = new TanStackFormController<Employee>(this, {
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    onSubmit({ value }) {
      // Do something with form data
      console.log(value)
    },
  })
}
7
Note that this references the instance of your LitElement in which you want to use TanStack Form.
8
Create Form Fields
9
Use the field method to wire up form elements. The first parameter is FieldOptions and the second is a callback to render your element:
10
render() {
  return html`
    <form
      @submit=${(e: Event) => {
        e.preventDefault()
        e.stopPropagation()
        this.#form.api.handleSubmit()
      }}
    >
      ${this.#form.field(
        {
          name: 'firstName',
          validators: {
            onChange: ({ value }) =>
              value.length < 3 ? 'Not long enough' : undefined,
          },
        },
        (field) => {
          return html`
            <div>
              <label for="${field.name}">First Name</label>
              <input
                id="${field.name}"
                type="text"
                placeholder="First Name"
                .value="${field.state.value}"
                @blur="${() => field.handleBlur()}"
                @input="${(e: Event) => {
                  const target = e.target as HTMLInputElement
                  field.handleChange(target.value)
                }}"
              />
            </div>
          `
        },
      )}
      <button type="submit">Submit</button>
    </form>
  `
}

Complete Example

Here’s a complete working example with two fields:
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
import { repeat } from 'lit/directives/repeat.js'

@customElement('tanstack-form-demo')
export class TanStackFormDemo extends LitElement {
  #form = new TanStackFormController(this, {
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    onSubmit({ value }) {
      console.log(value)
    },
  })

  render() {
    return html`
      <form
        @submit=${(e: Event) => {
          e.preventDefault()
          e.stopPropagation()
          this.#form.api.handleSubmit()
        }}
      >
        ${this.#form.field(
          {
            name: 'firstName',
            validators: {
              onChange: ({ value }) =>
                !value
                  ? 'A first name is required'
                  : value.length < 3
                    ? 'First name must be at least 3 characters'
                    : undefined,
            },
          },
          (field) => {
            return html`
              <div>
                <label for="${field.name}">First Name:</label>
                <input
                  id="${field.name}"
                  name="${field.name}"
                  .value="${field.state.value}"
                  @blur="${() => field.handleBlur()}"
                  @input="${(e: Event) => {
                    const target = e.target as HTMLInputElement
                    field.handleChange(target.value)
                  }}"
                />
                ${field.state.meta.isTouched && !field.state.meta.isValid
                  ? html`${repeat(
                      field.state.meta.errors,
                      (__, idx) => idx,
                      (error) => {
                        return html`<div style="color: red;">${error}</div>`
                      },
                    )}`
                  : nothing}
              </div>
            `
          },
        )}

        ${this.#form.field(
          {
            name: 'lastName',
          },
          (field) => {
            return html`
              <div>
                <label for="${field.name}">Last Name:</label>
                <input
                  id="${field.name}"
                  name="${field.name}"
                  .value="${field.state.value}"
                  @blur="${() => field.handleBlur()}"
                  @input="${(e: Event) => {
                    const target = e.target as HTMLInputElement
                    field.handleChange(target.value)
                  }}"
                />
              </div>
            `
          },
        )}

        <button type="submit" ?disabled=${this.#form.api.state.isSubmitting}>
          ${this.#form.api.state.isSubmitting ? '...' : 'Submit'}
        </button>
        <button type="button" @click=${() => this.#form.api.reset()}>
          Reset
        </button>
      </form>
    `
  }
}

Next Steps

Now that you have a basic form working, you can explore more advanced features:

Build docs developers (and LLMs) love