Frameworks
One field across React Hook Form, TanStack, Vue, and more.
Prefer the live demos? Open this section in the interactive docs.
One field, every framework: React Hook Form, TanStack Form, Formik, Vue, Angular, shadcn/ui, vanilla, and a <lingo-input> web component.
The docs show one live preview field, then a grouped integration picker with actual SVG brand logos and brand-color accents for each snippet. Vanilla, React, Next.js, React Hook Form, TanStack Form, shadcn/ui, Vue, Svelte, Node, agents, and the web component all feed the same field contract.
The form-library snippets use the same field contract: standardSchemaResolver(lingoObject(shape, { passthrough: true })) for whole-form React Hook Form and shadcn/ui forms, validators.onChange plus a useLingoInput bridge for TanStack Form, and defineLingoInput() for the form-associated <lingo-input> element.
Caption: The code swaps adapters; the field contract stays the same.
Vanilla
<form method="post" action="/signup" novalidate>
<label for="height">Height</label>
<input
id="height"
name="height"
inputmode="text"
placeholder="5'11" or 180cm"
required
/>
<p id="height-error" role="alert"></p>
<p id="height-hint" aria-hidden="true"></p>
<button>Continue</button>
</form>
<script type="module">
import { lingoInput } from "@pascal-app/lingo/dom"
lingoInput(document.querySelector("#height"), {
kind: "length",
unit: "m",
name: "height",
min: "0.3m",
max: "2.5m",
required: true,
validationBehavior: "aria",
errorElement: "#height-error",
hintElement: "#height-hint",
})
</script>React
import { useLingoInput } from "@pascal-app/lingo/react"
function HeightField() {
const field = useLingoInput({
kind: "length",
unit: "m",
name: "height_m",
})
return <input ref={field.ref} placeholder="5'11\" or 180cm" />
}React Hook Form
import {
standardSchemaResolver,
} from "@hookform/resolvers/standard-schema"
import { useForm } from "react-hook-form"
import {
dateField,
lingoObject,
quantityField,
} from "@pascal-app/lingo/ai"
const shape = {
weight_kg: quantityField({
kind: "mass",
unit: "kg",
min: 0,
}),
visit_date: dateField({ now: new Date() }),
}
const form = useForm({
resolver: standardSchemaResolver(
lingoObject(shape, { passthrough: true }),
),
})
form.handleSubmit((data) => {
// data.weight_kg is a number in kg.
// data.visit_date is an ISO string.
})TanStack
import { quantityField } from "@pascal-app/lingo/ai"
import { useLingoInput } from "@pascal-app/lingo/react"
const heightField = quantityField({
kind: "length",
unit: "m",
min: 0.3,
max: 2.5,
})
function HeightBridge({ field }) {
const { ref, state } = useLingoInput({
kind: "length",
unit: "m",
value: field.state.value ?? null,
onValueChange: (value) => field.handleChange(value),
})
return (
<input
ref={ref}
data-state={state}
onBlur={field.handleBlur}
/>
)
}
<form.Field
name="height_m"
validators={{ onChange: heightField }}
>
{(field) => <HeightBridge field={field} />}
</form.Field>shadcn/ui
import {
standardSchemaResolver,
} from "@hookform/resolvers/standard-schema"
import { Controller, useForm } from "react-hook-form"
import {
lingoObject,
quantityField,
} from "@pascal-app/lingo/ai"
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
const schema = lingoObject({
height: quantityField({
kind: "length",
unit: "m",
min: 0.3,
max: 2.5,
}),
})
const form = useForm({
resolver: standardSchemaResolver(schema),
defaultValues: { height: "" },
})
<Controller
name="height"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Height</FieldLabel>
<Input
{...field}
id={field.name}
aria-invalid={fieldState.invalid}
placeholder="5'11" or 180cm"
/>
<FieldDescription>
Any format - imperial or metric.
</FieldDescription>
{fieldState.invalid ? (
<FieldError errors={[fieldState.error]} />
) : null}
</Field>
)}
/>Web component
<script type="module">
import { defineLingoInput } from "@pascal-app/lingo/element"
defineLingoInput()
</script>
<form>
<label for="height">Height</label>
<lingo-input
id="height"
name="height_m"
kind="length"
unit="m"
></lingo-input>
</form>
<!-- after committing 5'11", -->
<!-- the form submits height_m=1.8034 -->Next action
"use server"
import { parseQuantity } from "@pascal-app/lingo"
export async function validateLength(_prev, formData) {
const input = String(formData.get("length") ?? "")
const result = parseQuantity(input, {
kind: "length",
unit: "m",
strictness: "confirm",
accept: { ranges: false, conversions: false },
})
if (!result.ok) {
return { ok: false, issues: result.issues }
}
return {
ok: true,
canonicalMeters: result.quantity.base,
formatted: result.quantity.format({ unit: "m" }),
}
}Vue
<script setup>
import { onBeforeUnmount, onMounted, ref } from "vue"
import { lingoInput } from "@pascal-app/lingo/dom"
const el = ref(null)
let field
onMounted(() => {
field = lingoInput(el.value, {
kind: "mass",
unit: "kg",
name: "weight_kg",
})
})
onBeforeUnmount(() => field?.destroy())
</script>
<template>
<input ref="el" placeholder="2 lb 3 oz" />
</template>Svelte
<script>
import { lingoInput } from "@pascal-app/lingo/dom"
export function lingo(node, options) {
const field = lingoInput(node, options)
return {
update(next) { field.update(next) },
destroy() { field.destroy() },
}
}
</script>
<input use:lingo={{ kind: "length", unit: "m", name: "height_m" }} />Node
import { parseQuantity } from "@pascal-app/lingo"
export function validatePayload(payload) {
const result = parseQuantity(payload.length, {
kind: "length",
unit: "m",
strictness: "confirm",
accept: { ranges: false, conversions: false },
})
if (!result.ok) {
return {
ok: false,
issues: result.issues,
candidate: result.candidate,
}
}
return { ok: true, meters: result.quantity.base }
}Agents
// llms.txt tells agents to pass strings, not floats.
// A browser automation agent can fill the field either way:
lingoInput.get(document.querySelector("#height"))?.set("5'11\"")
// Or it can type natural language. The hidden input named height_m
// carries the canonical value for normal form submission.