Forms and inputsForm
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callForm
A project brief with inline checks and an agent that drafts the rest. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add formReact
npx designme add form --reactThe CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.
About
What it is for and what it does.
Form is a short form, set up as a project brief by default, with a Draft with agent button that fills the fields in for the person. Fields the agent filled carry a Drafted mark beside their label until the person edits them, so it is clear what still needs a human read. On submit every field is checked, and a confirmation replaces the form once it goes through.
- When an agent can fill in a brief, ticket or request from context and a person should review it
- When people need to see which answers they typed and which the agent wrote
- When a short form needs inline messages per field instead of a page of errors
- When creating something should hand off to the agent, as in "the agent is gathering references now"
- Fields can be text, textarea or
choice, a row of chips that picks one value. - A
maxLengthon a text field shows a live counter like 0/40 and stops typing there. - Passing
onDraftadds the Draft with agent button, which shows Drafting and then Drafted while your promise runs. - Text fields that change while the draft runs are marked Drafted until the person edits them; choice fields are not marked.
- Each field's
validateruns on submit, shows its message under the field, and clears as soon as the value changes. - If
onSubmitreturns a promise, the button spins until it settles; a rejection shows its message, and success shows the done screen.
- Field messages render with
role="alert", and invalid inputs setaria-invalid. - Choice chips sit in a
grouplabelled with the field name, and each chip reportsaria-pressed. - The Drafted mark beside a label is
aria-hidden, so it is not read into the field's name. - The confirmation after submit has
role="status".
Usage
Import it and drive it from your own state.
Import
import { Form } from '@/components/designme/form/Form';Example
The demo above as a React component, 44 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useRef, useState } from 'react';
import { useScript, useTakeover, useWait } from '../lib/useScript';
import { Form, type FormField, type FormHandle, type FormValues } from './Form';
const FIELDS: FormField[] = [
{ key: 'name', label: 'Project name', placeholder: 'Pricing refresh', maxLength: 40, validate: v => !v.trim() && 'Give it a name' },
{ key: 'goal', label: 'Goal', type: 'textarea', rows: 2, placeholder: 'What should change, and why?', validate: v => v.trim().length < 12 && 'Add a sentence about the goal' },
{ key: 'type', label: 'Type', type: 'choice', options: [{ value: 'web', label: 'Website' }, { value: 'app', label: 'App' }, { value: 'brand', label: 'Brand' }], validate: v => !v && 'Pick a type' },
{ key: 'due', label: 'Due', placeholder: 'Nov 14' }
];
/* ---------- Form: a project brief; the agent can draft it, and submit checks every field ---------- */
export default function FormDemo() {
const [values, setValues] = useState<FormValues>({});
const latest = useRef<FormValues>({});
const set = (v: FormValues) => { latest.current = v; setValues(v); };
const wait = useWait();
// the agent types each field in, a letter at a time
const typeIn = async (key: string, text: string) => {
set({ ...latest.current, [key]: '' });
for (let i = 1; i <= text.length; i++) { set({ ...latest.current, [key]: text.slice(0, i) }); await wait(18); }
};
const draft = async () => {
if (!latest.current.name) await typeIn('name', 'Pricing refresh');
await typeIn('goal', 'Lift trial to paid by showing annual plans first and keeping one CTA per card.');
if (!latest.current.type) set({ ...latest.current, type: 'web' });
if (!latest.current.due) await typeIn('due', 'Nov 14');
};
// a moment after it appears, the agent drafts the brief, unless someone has started on it
// the preview has the agent fill it in once on its own, unless you take over first
const form = useRef<FormHandle>(null), ref = useRef<HTMLDivElement>(null), mine = useTakeover(ref);
useScript(async s => { await s.wait(1400); if (!mine() && !latest.current.name) await form.current?.draft(); });
return (
<div ref={ref} style={{ width: '100%' }}>
<Form ref={form} fields={FIELDS} values={values} onValuesChange={set} onDraft={draft} onSubmit={() => wait(800)} />
</div>
);
}
Props
Everything Form takes.
| Prop | Type | What it does |
|---|---|---|
title | ReactNode | Defaults to "New project brief" |
fieldsrequired | FormField[] | |
values | FormValues | The values by field key. Leave it out to let the fields hold their own |
defaultValues | FormValues | |
onValuesChange | (values: FormValues) => void | |
onDraft | () => void | Promise<void> | Adds "Draft with agent". Fill the values in while the promise runs; the fields it fills are marked Drafted beside their label until someone edits them |
draftLabel | string | |
draftingLabel | string | |
draftedLabel | string | |
draftedMessage | string | Shown beside the submit button after a draft. Defaults to "Review what the agent drafted" |
onSubmit | (values: FormValues) => void | Promise<void> | Called with the values once every field passes. Return a promise and the button spins until it settles |
submitLabel | string | |
invalidMessage | string | Shown beside the submit button when a field needs a look |
errorMessage | string | Shown when onSubmit rejects without a message of its own |
doneTitle | (values: FormValues) => ReactNode | The line after a submit. Defaults to "<first field> created" |
doneDetail | ReactNode | |
className | string | |
ref | Ref<FormHandle> | Gives access to draft(), to start the agent's draft without a click |
Code
The full source, as the CLI installs it.
Form.tsxReact · 175 lines
'use client';
import { useEffect, useImperativeHandle, useRef, useState, type FormEvent, type ReactNode, type Ref } from 'react';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { enter, Spin, Swap, Think } from '../lib/motion';
import '../designme.css';
import './form.css';
export type FormValues = Record<string, string>;
/** What a ref to the form can do */
export interface FormHandle {
/** Runs onDraft as if "Draft with agent" were pressed, with the same Drafting and Drafted states */
draft: () => Promise<void>;
}
export interface FormField {
key: string;
/** Like "Project name" */
label: string;
/** text (the default), textarea, or choice: a row of chips that picks one */
type?: 'text' | 'textarea' | 'choice';
placeholder?: string;
/** Shows a counter like 0/40 and stops typing there */
maxLength?: number;
/** Rows for a textarea. Defaults to 2 */
rows?: number;
/** The chips of a choice field */
options?: { value: string; label: string }[];
autoComplete?: string;
/** Returns the message to show when the value won't do, like "Give it a name". Checked on submit */
validate?: (value: string, values: FormValues) => string | false | null | undefined;
}
export interface FormProps {
/** Defaults to "New project brief" */
title?: ReactNode;
fields: FormField[];
/** The values by field key. Leave it out to let the fields hold their own */
values?: FormValues;
defaultValues?: FormValues;
onValuesChange?: (values: FormValues) => void;
/** Adds "Draft with agent". Fill the values in while the promise runs; the fields it fills are marked Drafted beside their label until someone edits them */
onDraft?: () => void | Promise<void>;
draftLabel?: string;
draftingLabel?: string;
draftedLabel?: string;
/** Shown beside the submit button after a draft. Defaults to "Review what the agent drafted" */
draftedMessage?: string;
/** Called with the values once every field passes. Return a promise and the button spins until it settles */
onSubmit?: (values: FormValues) => void | Promise<void>;
submitLabel?: string;
/** Shown beside the submit button when a field needs a look */
invalidMessage?: string;
/** Shown when onSubmit rejects without a message of its own */
errorMessage?: string;
/** The line after a submit. Defaults to "<first field> created" */
doneTitle?: (values: FormValues) => ReactNode;
doneDetail?: ReactNode;
className?: string;
/** Gives access to draft(), to start the agent's draft without a click */
ref?: Ref<FormHandle>;
}
/** A short form an agent can draft for you: fields that check themselves on submit, and a confirmation. */
export function Form({
title = 'New project brief', fields, values, defaultValues = {}, onValuesChange, onDraft, draftLabel = 'Draft with agent', draftingLabel = 'Drafting',
draftedLabel = 'Drafted', draftedMessage = 'Review what the agent drafted', onSubmit, submitLabel = 'Create brief', invalidMessage = 'A few fields need a look',
errorMessage = 'Something went wrong, try again', doneTitle, doneDetail = 'The agent is gathering references now', className, ref
}: FormProps) {
const [vals, setVals] = useControllable(values, defaultValues, onValuesChange);
const [errors, setErrors] = useState<Record<string, string>>({});
const [ai, setAi] = useState<string[]>([]);
const [draft, setDraft] = useState<'idle' | 'drafting' | 'drafted'>('idle');
const [message, setMessage] = useState('');
const [busy, setBusy] = useState(false);
const [done, setDone] = useState(false);
const own = useRef<string | null>(null);
// a changed value clears its field's message, whoever changed it; while a draft runs, the text fields it fills are marked Drafted
// until the person edits them
const before = useRef(vals);
useEffect(() => {
const was = before.current; before.current = vals;
const edited = own.current; own.current = null;
const changed = fields.filter(f => f.key !== edited && (was[f.key] ?? '') !== (vals[f.key] ?? '')).map(f => f.key);
if (!changed.length) return;
setErrors(e => Object.fromEntries(Object.entries(e).filter(([k]) => !changed.includes(k))));
const filled = draft === 'drafting' ? changed.filter(k => fields.find(f => f.key === k)?.type !== 'choice') : [];
if (filled.length) setAi(a => [...new Set([...a, ...filled])]);
}, [vals]); // eslint-disable-line react-hooks/exhaustive-deps
const set = (key: string, value: string) => {
own.current = key;
setVals({ ...vals, [key]: value });
setErrors(({ [key]: _, ...rest }) => rest);
setAi(a => a.filter(k => k !== key));
};
const runDraft = async () => {
if (!onDraft || draft === 'drafting') return;
setDraft('drafting');
try { await onDraft(); } finally { setDraft('drafted'); }
setMessage(draftedMessage);
};
useImperativeHandle(ref, () => ({ draft: runDraft }));
const submit = async (e: FormEvent) => {
e.preventDefault();
if (busy) return;
const found = { ...errors };
for (const f of fields) { const msg = f.validate?.(vals[f.key] ?? '', vals); if (msg && !found[f.key]) found[f.key] = msg; }
setErrors(found);
if (fields.some(f => f.validate?.(vals[f.key] ?? '', vals))) { setMessage(invalidMessage); return; }
setBusy(true);
try { await onSubmit?.(vals); } catch (err) { setBusy(false); setMessage(err instanceof Error && err.message ? err.message : errorMessage); return; }
setBusy(false); setDone(true);
};
if (done) {
const first = fields[0] ? vals[fields[0].key] : '';
return (
<form data-dm="" className={cx('dm-fmx dm-panel', className)} noValidate onSubmit={e => e.preventDefault()}>
<div className="dm-fmx-done" role="status">
<Icon name="ok" className="dm-enter" />
<b {...enter(70)}>{doneTitle ? doneTitle(vals) : `${first} created`}</b>
<span className="dm-muted dm-enter" style={enter(140).style}>{doneDetail}</span>
</div>
</form>
);
}
return (
<form data-dm="" className={cx('dm-fmx dm-panel', className)} noValidate onSubmit={submit}>
<div className="dm-fmx-h">
<b>{title}</b>
{onDraft && (
<button type="button" className="dm-pill dm-soft dm-sm dm-fmx-ai" onClick={runDraft}>
{draft === 'drafting' ? <><Think />{draftingLabel}</> : draft === 'drafted' ? <><Icon name="check" size="xs" />{draftedLabel}</> : <><Icon name="spark" size="xs" />{draftLabel}</>}
</button>
)}
</div>
{fields.map(f => {
const value = vals[f.key] ?? '', err = errors[f.key];
const state = cx('dm-fmx-f', err && 'dm-bad', ai.includes(f.key) && 'dm-ai');
const note = err && <span className="dm-er" role="alert">{err}</span>;
if (f.type === 'choice') return (
<div key={f.key} className={state}>
<span>{f.label}</span>
<div className="dm-fmx-ty" role="group" aria-label={f.label}>
{f.options?.map(o => <button type="button" key={o.value} className={cx('dm-chip dm-sm', value === o.value && 'dm-on')} aria-pressed={value === o.value} onClick={() => set(f.key, o.value)}>{o.label}</button>)}
</div>
{note}
</div>
);
return (
<label key={f.key} className={state}>
<span>{f.label}<i className="dm-fmx-dr" aria-hidden="true"><Icon name="spark" size="xs" />{draftedLabel}</i></span>
{f.type === 'textarea'
? <textarea name={f.key} rows={f.rows ?? 2} placeholder={f.placeholder} maxLength={f.maxLength} autoComplete={f.autoComplete} aria-invalid={!!err || undefined} value={value} onChange={e => set(f.key, e.target.value)} />
: <input name={f.key} placeholder={f.placeholder} maxLength={f.maxLength} autoComplete={f.autoComplete} aria-invalid={!!err || undefined} value={value} onChange={e => set(f.key, e.target.value)} />}
{f.maxLength && f.type !== 'textarea' && <em className="dm-fmx-c dm-num">{value.length}/{f.maxLength}</em>}
{note}
</label>
);
})}
<div className="dm-fmx-ft">
<Swap className="dm-fmx-ms dm-muted">{message}</Swap>
<button type="submit" className="dm-pill dm-primary dm-sm dm-fmx-go">{busy ? <Spin /> : submitLabel}</button>
</div>
</form>
);
}
form.cssReact · 28 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Form ---------- */
.dm-fmx{padding:var(--dm-s-4);display:flex;flex-direction:column;gap:var(--dm-s-3);max-width:380px;margin:0 auto;width:100%}
.dm-fmx-h{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-2)}
.dm-fmx-h b{font-weight:500;font-size:14px}
.dm-fmx-f{display:flex;flex-direction:column;gap:5px;font-size:12.5px;font-weight:500;position:relative}
.dm-fmx-f input,.dm-fmx-f textarea{border:0;outline:0;border-radius:var(--dm-r-field);background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line) 50%,transparent);padding:10px var(--dm-s-3);font:inherit;font-size:13.5px;line-height:20px;font-weight:400;color:var(--dm-fg);resize:none;transition:box-shadow var(--dm-t-fast)}
@supports (corner-shape:squircle){.dm-fmx-f :is(input,textarea){corner-shape:squircle;border-radius:var(--dm-r-field-sq)}}
.dm-fmx-f input::placeholder,.dm-fmx-f textarea::placeholder{color:var(--dm-muted)}
.dm-fmx-f input:focus,.dm-fmx-f textarea:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-field-line-strong) 50%,transparent)}
/* a field marked wrong keeps its red edge, focused or not, until it is fixed; the message says what is wrong */
.dm-fmx-f.dm-bad input,.dm-fmx-f.dm-bad textarea{box-shadow:inset 0 0 0 1px var(--dm-bad)}
.dm-fmx-f .dm-er{font-size:11.5px;color:var(--dm-bad);font-weight:400;animation:dm-enter .35s var(--dm-ease) backwards}
/* what the agent drafted says so quietly beside its label, until someone edits it */
.dm-fmx-f>span:first-child{display:flex;align-items:center;gap:var(--dm-s-2)}
.dm-fmx-dr{display:inline-flex;align-items:center;gap:3px;font-style:normal;font-size:11px;font-weight:400;color:var(--dm-muted);opacity:0;transform:translateX(-3px);transition:opacity var(--dm-t) var(--dm-ease),transform var(--dm-t) var(--dm-ease)}
.dm-fmx-dr .dm-i{color:var(--dm-accent-fg,var(--dm-accent))}
.dm-fmx-f.dm-ai .dm-fmx-dr{opacity:1;transform:none}
.dm-fmx-c{position:absolute;right:12px;top:24px;height:40px;display:flex;align-items:center;font-style:normal;font-size:11px;color:var(--dm-muted);font-weight:400;pointer-events:none}
.dm-fmx-ty{display:flex;gap:6px}
/* the types are white chips with a dimmed grey edge; the picked one's edge is a step stronger */
.dm-fmx-ty .dm-chip{background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-pick-line) 50%,transparent)}
@media (hover:hover) and (pointer:fine){.dm-fmx-ty .dm-chip:hover{background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-faint) 50%,transparent)}}
.dm-fmx-ft{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-2);margin-top:var(--dm-s-1)}
.dm-fmx-ms{font-size:12px}
.dm-fmx-done{display:flex;flex-direction:column;align-items:center;gap:6px;padding:var(--dm-s-6) 0;text-align:center;font-size:13px}
.dm-fmx-done>svg{width:28px;height:28px;color:var(--dm-ok)}
form.htmlHTML · 13 lines
<div class="card" data-c="form">
<div class="stage-in">
<form class="fmx panel" novalidate>
<div class="fmx-h"><b>New project brief</b><button type="button" class="pill soft sm fmx-ai"><svg class="i xs"><use href="#i-spark"/></svg>Draft with agent</button></div>
<label class="fmx-f"><span>Project name<i class="fmx-dr" aria-hidden="true"><svg class="i xs"><use href="#i-spark"/></svg>Drafted</i></span><input name="n" placeholder="Pricing refresh" maxlength="40"><em class="fmx-c num">0/40</em></label>
<label class="fmx-f"><span>Goal<i class="fmx-dr" aria-hidden="true"><svg class="i xs"><use href="#i-spark"/></svg>Drafted</i></span><textarea name="g" rows="2" placeholder="What should change, and why?"></textarea></label>
<div class="fmx-f"><span>Type</span><div class="fmx-ty"><button type="button" class="chip sm" data-v="web">Website</button><button type="button" class="chip sm" data-v="app">App</button><button type="button" class="chip sm" data-v="brand">Brand</button></div></div>
<label class="fmx-f"><span>Due<i class="fmx-dr" aria-hidden="true"><svg class="i xs"><use href="#i-spark"/></svg>Drafted</i></span><input name="d" type="text" placeholder="Nov 14"></label>
<div class="fmx-ft"><span class="fmx-ms muted"></span><button class="pill primary sm fmx-go" type="submit">Create brief</button></div>
</form>
</div>
</div>
form.cssHTML · 27 lines
/* ---------- Form ---------- */
.fmx{padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-3);max-width:380px;margin:0 auto;width:100%}
.fmx-h{display:flex;align-items:center;justify-content:space-between;gap:var(--s-2)}
.fmx-h b{font-weight:500;font-size:14px}
.fmx-f{display:flex;flex-direction:column;gap:5px;font-size:12.5px;font-weight:500;position:relative}
.fmx-f input,.fmx-f textarea{border:0;outline:0;border-radius:var(--r-field);background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line) 50%,transparent);padding:10px var(--s-3);font:inherit;font-size:13.5px;line-height:20px;font-weight:400;color:var(--fg);resize:none;transition:box-shadow var(--t-fast)}
@supports (corner-shape:squircle){.fmx-f :is(input,textarea){corner-shape:squircle;border-radius:var(--r-field-sq)}}
.fmx-f input::placeholder,.fmx-f textarea::placeholder{color:var(--muted)}
.fmx-f input:focus,.fmx-f textarea:focus{box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--field-line-strong) 50%,transparent)}
/* a field marked wrong keeps its red edge, focused or not, until it is fixed; the message says what is wrong */
.fmx-f.bad input,.fmx-f.bad textarea{box-shadow:inset 0 0 0 1px var(--bad)}
.fmx-f .er{font-size:11.5px;color:var(--bad);font-weight:400;animation:enter .35s var(--ease) backwards}
/* what the agent drafted says so quietly beside its label, until someone edits it */
.fmx-f>span:first-child{display:flex;align-items:center;gap:var(--s-2)}
.fmx-dr{display:inline-flex;align-items:center;gap:3px;font-style:normal;font-size:11px;font-weight:400;color:var(--muted);opacity:0;transform:translateX(-3px);transition:opacity var(--t) var(--ease),transform var(--t) var(--ease)}
.fmx-dr .i{color:var(--accent-fg,var(--accent))}
.fmx-f.ai .fmx-dr{opacity:1;transform:none}
.fmx-c{position:absolute;right:12px;top:24px;height:40px;display:flex;align-items:center;font-style:normal;font-size:11px;color:var(--muted);font-weight:400;pointer-events:none}
.fmx-ty{display:flex;gap:6px}
/* the types are white chips with a dimmed grey edge; the picked one's edge is a step stronger */
.fmx-ty .chip{background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--pick-line) 50%,transparent)}
@media (hover:hover) and (pointer:fine){.fmx-ty .chip:hover{background:var(--surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--faint) 50%,transparent)}}
.fmx-ft{display:flex;align-items:center;justify-content:space-between;gap:var(--s-2);margin-top:var(--s-1)}
.fmx-ms{font-size:12px}
.fmx-done{display:flex;flex-direction:column;align-items:center;gap:6px;padding:var(--s-6) 0;text-align:center;font-size:13px}
.fmx-done>svg{width:28px;height:28px;color:var(--ok)}
form.jsHTML · 30 lines
DM.def('form', (r, c) => {
r.innerHTML = r.innerHTML.split('{spark}').join(I('spark','xs'));
const form = $('.fmx', r), n = $('[name=n]', r), g = $('[name=g]', r), d = $('[name=d]', r), cnt = $('.fmx-c', r), ms = $('.fmx-ms', r);
let type = null;
n.oninput = () => { cnt.textContent = n.value.length + '/40'; clear(n); };
g.oninput = () => clear(g); d.oninput = () => clear(d);
const chips = $$('.fmx-ty .chip', r);
chips.forEach(ch => ch.onclick = () => { type = ch.dataset.v; chips.forEach(x => x.classList.toggle('on', x === ch)); clear(ch.parentElement); });
function clear(el){ const f = el.closest('.fmx-f'); f.classList.remove('bad', 'ai'); $('.er', f)?.remove(); }
function bad(el, t){ const f = el.closest('.fmx-f'); if (f.classList.contains('bad')) return; f.classList.add('bad'); f.insertAdjacentHTML('beforeend', `<span class="er">${t}</span>`); }
async function type_(el, s){ el.closest('.fmx-f').classList.add('ai'); el.value = ''; for (const ch of s){ el.value += ch; el.dispatchEvent(new Event('input')); el.closest('.fmx-f').classList.add('ai'); await c.wait(18); } }
$('.fmx-ai', r).onclick = () => c.run(async () => {
const b = $('.fmx-ai', r); b.innerHTML = '<span class="think on"><i></i><i></i><i></i><i></i></span>Drafting';
if (!n.value) await type_(n, 'Pricing refresh');
await type_(g, 'Lift trial to paid by showing annual plans first and keeping one CTA per card.');
if (!type) chips[0].click();
if (!d.value) await type_(d, 'Nov 14');
b.innerHTML = I('check','xs') + 'Drafted'; swap(ms, 'Review what the agent drafted');
});
// the preview has the agent fill it in once on its own, unless you take over first
c.auto(1400, () => { if (!n.value) $('.fmx-ai', r).click(); });
form.onsubmit = e => { e.preventDefault(); let ok = true;
if (!n.value.trim()){ bad(n, 'Give it a name'); ok = false; }
if (g.value.trim().length < 12){ bad(g, 'Add a sentence about the goal'); ok = false; }
if (!type){ bad(chips[0], 'Pick a type'); ok = false; }
if (!ok) return swap(ms, 'A few fields need a look');
c.run(async () => { const go = $('.fmx-go', r); go.innerHTML = '<span class="spin"></span>'; await c.wait(800);
hold(form); [...form.children].forEach(k => k.hidden = true); const dn = html(`<div class="fmx-done">${I('ok')}<b>${n.value} created</b><span class="muted">The agent is gathering references now</span></div>`); form.appendChild(dn); stagger([...dn.children], 70); }); };
});
Questions
What people ask about it.
- How does the agent fill in the fields?
- Control the form with
valuesandonValuesChange, and update the values inside youronDraftpromise. Any text field that changes while the draft runs gets the Drafted mark. - Can I start the agent's draft without a click?
- Yes. Put a ref on the form and call
ref.current.draft(). It runsonDraftwith the same Drafting and Drafted states as the button. - How do I validate a field against another field?
validatereceives the field's value and all the form's values. Return a message string to block the submit, or a falsy value to let it pass.
More in Forms and inputs
Buttons, fields, pickers and sign-in flows for any product.