How it works
Every part of an Active Form carries a stable hook class — ts-form-block, ts-form, ts-field and ts-form-message — beside its Tailwind utilities. Style forms by targeting those classes in your site's stylesheet; copy a view only when you need different markup.
- What renders the form. An editor adds the Active Form block to a page and picks a form. The block draws a wrapper (title and intro), and inside it a Livewire component draws the form itself, one view per question type.
- Where the default look comes from. The views also use Tailwind classes like
roundedandbg-neutral-950. Your site's Tailwind build only makes the classes it finds, so itstailwind.config.jsmust list the package's form views incontent(the install guide's Step 14 gives the three paths). - Inputs need the forms plugin. The text boxes, selects and checkboxes rely on
@tailwindcss/formsfor their base look and focus rings. Without it they fall back to browser defaults. - Scope your rules to win. A Tailwind utility is one class (specificity 0,1,0), and so is
.ts-field__input. Write.ts-form-block .ts-field__input(0,2,0) and your rule beats the utility wherever your stylesheet loads, without!important.
The Class reference lists every hook, and A starting stylesheet shows them in use.
The markup at a glance
Every form has the same skeleton. This is it with only the hook classes shown; 12 is the form's id and email a question's key.
<section class="ts-form-block" aria-labelledby="form-12-heading">
<div class="ts-form-block__inner"> <!-- width: max-w-3xl -->
<h2 class="ts-form-block__title" id="form-12-heading">Volunteer sign-up</h2>
<div class="ts-form-block__intro"><p>Intro paragraph…</p></div>
<div class="ts-form-block__form">
<div class="ts-form-root" wire:id="…">
<form class="ts-form" novalidate>
<p class="ts-form__step">Step 1 of 3</p> <!-- multi-page forms only -->
<div class="ts-form__errors" role="alert">Please check the answers marked below.</div>
<fieldset class="ts-form__section"> <!-- one per section -->
<legend class="ts-form__section-title">About you</legend>
<div class="ts-form__grid"> <!-- 2 columns from 640px -->
<div class="ts-field ts-field--email ts-field--half ts-field--required ts-field--invalid">
<label class="ts-field__label" for="form-12-email">
Email address <span class="ts-field__required">*</span>
</label>
<input class="ts-field__input" id="form-12-email" type="email" aria-invalid="true">
<p class="ts-field__help" id="form-12-email-help">We only use it to reply.</p>
<p class="ts-field__error" id="form-12-email-error">Enter an email address.</p>
</div>
</div>
</fieldset>
<div class="ts-form__captcha"><!-- robot check, last page only --></div>
<div aria-hidden="true"><!-- spam trap: no hook, keep it off screen --></div>
<div class="ts-form__actions">
<button class="ts-form__back" type="button">Back</button>
<button class="ts-form__submit" type="submit">Send</button>
</div>
</form>
</div>
</div>
</div>
</section>
After sending, the <form> is replaced by the thank-you box; a closed or full form shows a message box instead (see States).
Class reference
Target these hook classes, never the utility classes beside them: utilities can change between package versions, hooks don't. Prefix your rules with .ts-form-block so they win over the utilities.
| Part | Hook class | Default look |
|---|---|---|
| The block | ts-form-block |
48px space above and below |
| Block content | ts-form-block__inner |
centred, 768px wide at most |
| Form title | ts-form-block__title |
36px display font, near-black |
| Intro text | ts-form-block__intro |
18px, grey |
| The form | ts-form |
column, 24px gaps |
| Step counter | ts-form__step |
small, grey, medium weight |
| Error summary | ts-form__errors |
pale red box, dark red text |
| Section of questions | ts-form__section |
column, 16px gaps |
| Section title | ts-form__section-title |
20px, medium weight |
| Question grid | ts-form__grid |
1 column, 2 from 640px; 16px × 20px gaps |
| One question | ts-field |
full width unless set to half |
| Label (or choice legend) | ts-field__label |
14px, medium weight |
| Required star | ts-field__required |
dark red " *" |
| Text box, select, date, file | ts-field__input |
full width, grey border, small shadow, rounded |
| Option list | ts-field__options |
column, 8px gaps (Yes or no: a row) |
| One option | ts-field__option |
the box, then the text |
| Checkbox or radio | ts-field__option-input |
near-black when ticked |
| Option text | ts-field__option-label |
near-black |
| Help text | ts-field__help |
14px, grey |
| Error message | ts-field__error |
14px, dark red |
| Robot check | ts-form__captcha |
Cloudflare's own box |
| Buttons row | ts-form__actions |
row, 12px gaps |
| Back button | ts-form__back |
outlined |
| Send / Next button | ts-form__submit |
near-black fill, white text |
Each question also carries modifiers: ts-field--<type> (see Question types), ts-field--full or ts-field--half, ts-field--required when it must be answered, and ts-field--invalid when its answer failed a check.
Each question's input has the id form-<form id>-<question key>, so one question can be styled on its own: #form-12-email. Question keys don't change once a form is published.
Question types
Editors pick from 14 types. Eight are a label plus one control, Heading and Text show words only, and the three choice types wrap their options in a fieldset with a legend.
| Type (as editors see it) | Question class | What it renders | Notes for styling |
|---|---|---|---|
| Short answer | ts-field--text |
label + input[type=text] |
may carry maxlength |
| Long answer | ts-field--textarea |
label + textarea (5 rows) |
|
| Email address | ts-field--email |
label + input[type=email] |
|
| Phone number | ts-field--phone |
label + input[type=tel] |
|
| Number | ts-field--number |
label + input[type=number] |
may carry min, max, step |
| Date | ts-field--date |
label + input[type=date] |
the calendar popup is the browser's own |
| Dropdown | ts-field--select |
label + select |
first option is "Choose…" with an empty value |
| Multiple choice | ts-field--radio |
fieldset, legend, then ts-field__options of radios |
options stack vertically, 8px apart |
| Checkboxes | ts-field--checkboxes |
as Multiple choice, with checkboxes | |
| Yes or no | ts-field--yes-no |
as Multiple choice, two radios in a row | 24px between Yes and No |
| Agreement | ts-field--consent |
one ts-field__option: the box, then the label |
the box comes before the text |
| File upload | ts-field--file |
label + input[type=file], ts-field__limits, ts-field__uploading while sending, ts-field__files list |
style the button with .ts-field__input::file-selector-button |
| Heading | ts-field--heading |
ts-field__heading (an h3), then optional help |
not a question: no label or error |
| Text | ts-field--text-block |
ts-field__text: paragraphs editors write |
not a question |
Width is set per question in the form builder: full width spans both columns, half width takes one. Below 640px every question is full width.
States
Design every state below, not only the empty form: errors and the thank-you box are what people remember.
| State | Hook | What changes |
|---|---|---|
| Error on a question | ts-field--invalid on the question, ts-field__error below the input |
only the message is red by default; give the input a red border yourself |
| Errors on the page | ts-form__errors at the top of the form |
appears after a failed send; screen readers announce it |
| Required | ts-field--required on the question, ts-field__required in the label |
a red " *"; screen readers hear "(required)" from a hidden span |
| Focus | .ts-field__input:focus-visible, .ts-form__submit:focus-visible |
the forms plugin draws a near-black ring; keep a visible focus style |
| Sending | .ts-form__submit:disabled |
both buttons are disabled while answers are sent; Send fades to 60% |
| Uploading a file | ts-field__uploading |
shown during the upload; ts-field__files hides until it ends |
| Sent | ts-form-message--sent replaces the form |
ts-form-message__title "Thank you.", ts-form-message__body, then ts-form-message__reference |
| Closed, full or not open yet | ts-form-message--unavailable |
one ts-form-message__body line from the form's settings |
| Sign in needed | the same box, plus ts-form-message__button |
only for forms limited to signed-in visitors |
Two things only editors see: amber boxes (.bg-amber-50) for a block with no form, and a "Preview of the draft" note in previews. Visitors never get them, so they need no design.
Multi-page forms, uploads and the robot check
- Pages. A form set to show one section at a time has a "Step 2 of 3" line at the top, a Back button from page 2 on, and Next in place of Send until the last page. The whole form re-renders between pages, so don't rely on CSS transitions between them.
- Uploads. The file input is the browser's own; only its button (
::file-selector-button) and the surrounding text can be styled reliably. Keep the limits line visible: it tells people the size and types allowed. - The robot check. When a form asks for one, a Cloudflare Turnstile box appears above the buttons on the last page. It is an iframe of fixed size (300 × 65px) in Cloudflare's light or dark theme; you can position it, not restyle it.
- The spam trap. A hidden text box sits off screen (
left: -9999px). Never show it, move it or give itdisplay: none: bots fill it in because it is in the page, and answers with it filled in are filed as spam (the sender still sees the thank-you box).
A starting stylesheet, and copying views
No setup needed. The hook classes are in every form, so restyling is plain CSS in your site's stylesheet. To style one form differently from the rest, target its question ids (#form-12-email) or wrap the block in a page section with its own class.
A starting point, using your own tokens:
.ts-form-block .ts-form-block__title { font-family: var(--font-display); color: var(--ink); }
.ts-form-block .ts-field__label { font: 600 0.875rem/1.4 var(--font-body); color: var(--ink); }
.ts-form-block .ts-field__required { color: var(--danger); }
.ts-form-block .ts-field__input { border: 1px solid var(--line); border-radius: 0.5rem; padding: 0.625rem 0.75rem; box-shadow: none; }
.ts-form-block .ts-field__input:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; box-shadow: none; }
.ts-form-block .ts-field--invalid .ts-field__input { border-color: var(--danger); }
.ts-form-block .ts-field__error { color: var(--danger); }
.ts-form-block .ts-field__option-input { accent-color: var(--brand); }
.ts-form-block .ts-form__submit { background: var(--brand); border-radius: 999px; padding: 0.75rem 1.5rem; }
.ts-form-block .ts-form__submit:disabled { opacity: 0.6; cursor: progress; }
.ts-form-block .ts-form-message--sent { border-color: var(--brand); background: var(--brand-tint); }
When CSS isn't enough (a different layout, new wrappers, your own class names), copy the views you need into the site. Its copies win over the package's:
| To change | Copy to the site at |
|---|---|
| The block: title, intro, width | resources/views/wrangler/components/active-form/default.blade.php |
| The form: steps, error summary, buttons, thank-you box | resources/views/vendor/textstem/livewire/forms/public-form.blade.php |
| One question type | resources/views/vendor/textstem/forms/fields/<type>.blade.php (e.g. short-text, radio-list, file-upload) |
| Every label, legend, help and error line | resources/views/vendor/textstem/forms/fields/partials/ |
In a copied view, keep every wire: attribute, id, for, aria-*, role and name, every ts- hook class, and the hidden spam-trap box: the form stops working or stops being accessible without them. A copy no longer gets the package's fixes, so copy as few views as you can and note them for the developer.
Before you hand over
Test with a real form that uses every question type, on a phone and a desktop.
- [ ] Tailwind
contentlists the three package paths, and@tailwindcss/formsis installed - [ ] Full and half width questions line up at 640px and above; everything stacks below
- [ ] Every input has a visible focus style when tabbed to
- [ ] A failed send shows the summary box and red borders on the questions with errors
- [ ] The Send button looks disabled while sending
- [ ] The thank-you box, a closed form and a sign-in box are all designed
- [ ] A multi-page form shows the step line, Back and Next
- [ ] The file upload button, limits line and file list are styled
- [ ] The robot check has room above the buttons
- [ ] Text and borders reach 4.5:1 contrast for text and 3:1 for input borders
- [ ] Rules target ts- hook classes, never utilities; any copied views are listed for the developer, with the reason for each