Sentence Form

A contact form written as a sentence. Blanks stretch as you type, picked options fly up into place while the words make room, and on send the form settles into the sentence you wrote.

  • Form
  • Input
  • Motion

Uses motion, clsx, tailwind-merge

Open in ChatGPTOpen in Claude
Start a projectField & Fold Studio
Hi, I'm from . We'd like help with , ideally by . Our budget is around , and you can reach me at.

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/sentence-form.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/sentence-form

Examples

Finished sections built with Sentence Form. Each one installs whole, with the component included.

Demo Request

An AI startup's demo request on a dark panel. Each team option brings its own grammar ("for just me", "for a company of 500+") and the tools are a pick-any list. Turn on Fail the next send to see the error path: the form keeps every answer and says so beside the button.

Book a demo
I want an agent that handles for . It should work with . Send the demo to.

Event RSVP

An invitation reply with a clause that depends on an answer. Pick "with a guest" and " named ___" rises in after it while the words after it glide aside; switch back and the clause folds away. The dinner choice starts filled in through a default value.

Thursday 19 June · 7 pm

Midsummer supper at the studio

Count me in. I'm , and I'm coming . I'll have the . Send the details to.

Props

childrenRequired

TypeReact.ReactNode | ((values: SentenceFormValues) => React.ReactNode)

The sentence, with a SentenceFormField for each blank. Text is split into words so they can glide; any other element moves as one word. Pass a function of the current values for a clause that depends on an answer.

onSubmit

Type(values: SentenceFormValues, formData: FormData) => void | Promise<unknown>

Called on send once every blank checks out, with the values of the blanks in the sentence and the form's FormData. Return a promise to hold the form in a pending state; if it rejects, errorMessage shows and nothing is lost.

success

TypeReact.ReactNode | ((sent: { values, edit, reset }) => React.ReactNode)Default"Sent. Thank you!"

The confirmation that rises where the button was after a send. As a function it gets what was sent, edit() to turn the sentence back into the form with the answers kept, and reset() to start over.

errorMessage

TypeReact.ReactNode

Shown beside the button, and announced, when onSubmit throws or its promise rejects.

submitLabel

TypeReact.ReactNodeDefault"Send"

The send button's label.

defaultValues

TypeSentenceFormValues

The values it starts with: a string per blank, or a string[] for multiple. A field's own defaultValue fills in the rest.

values

TypeSentenceFormValues

The values, to control the form from outside. Pair it with onValuesChange.

onValuesChange

Type(values: SentenceFormValues) => void

Called with every value whenever one changes.

locale

TypestringDefault"en-US"

Locale for joining picks with Intl.ListFormat ("a website, a brand, and motion"). Set explicitly, so the server and the browser agree.

className

Typestring

Classes for the form. Set the sentence's font, size and line height here; the options in the tray inherit its font so a picked label can fly up without changing typeface.

trayClassName

Typestring

Classes for the tray under the sentence: the active blank's label, hint, options and error.

submitClassName

Typestring

Classes for the send button. It reads data-ready="true" once the required blanks are filled, and stays quieter until then; style the filled state with data-[ready=true]:.

field.nameRequired

Typestring

SentenceFormField: the field's name in the values and in the submitted FormData.

field.labelRequired

Typestring

SentenceFormField: names the field for screen readers (a choice reads as "Budget, $25k"), and heads the tray while the blank is active.

field.placeholder

Typestring

SentenceFormField: the muted hint in the empty blank, like "your name". It sets the blank's width until something is typed or picked.

field.hint

TypeReact.ReactNode

SentenceFormField: a line in the tray beside the label, like "Only used to reply". Choices default to "Pick one" or "Pick any".

field.options

Type(string | { value: string; label: string })[]

SentenceFormField: makes the blank a choice. The options show as chips in the tray; a picked one flies up into the blank. Use { value, label } when the submitted value differs from the words.

field.multiple

TypebooleanDefaultfalse

SentenceFormField: lets several options be picked. The value becomes a string[] in the order they were picked, shown joined with list grammar, and the tray stays open until Enter, Tab or a click elsewhere.

field.custom

Typeboolean | stringDefaultfalse

SentenceFormField: adds an "Other" chip to a pick-one blank that turns the blank into a text input in place. Pass a string to rename the chip.

field.type

Type"text" | "email" | "tel" | "url"Default"text"

SentenceFormField: the input type of a text blank. email and url are checked on send.

field.required

TypebooleanDefaultfalse

SentenceFormField: must be filled, or for multiple have a pick, before the form sends. Enter skips past optional blanks left empty.

field.defaultValue

Typestring | string[]

SentenceFormField: the value it starts with, until it's changed.

field.autoComplete

Typestring

SentenceFormField: the autofill hint for a text blank, like "name", "organization" or "email".

field.className

Typestring

SentenceFormField: classes for the blank's words: the input, the picks, and the answer once sent.