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 rounded and bg-neutral-950. Your site's Tailwind build only makes the classes it finds, so its tailwind.config.js must list the package's form views in content (the install guide's Step 14 gives the three paths).
  • Inputs need the forms plugin. The text boxes, selects and checkboxes rely on @tailwindcss/forms for 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 it display: 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 content lists the three package paths, and @tailwindcss/forms is 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
esc