How We Built a Fully Accessible Multi-Step Form in Webflow

Share this post

Rebuild series building multi step forms in webflow

An accessible multi-step form needs three things a standard Webflow form doesn't give you. Each step is a labeled group of fields. Errors are announced and tied to the field that caused them. Focus moves to the right place every time the step changes. We built exactly that for the Accessibility Book Club's book review form, and this post walks through the markup, the validation script, and one production-only bug that nearly broke it.

You can try the live form on the A11Y Unraveled book page. It has three steps: Your Info, Book Review, and Submit.

Why multi-step forms are hard to make accessible

A multi-step form hides part of itself at all times. Sighted mouse users see the step change and keep going. Someone using a screen reader or a keyboard only knows what the code tells them. If focus stays on a "Next" button that has just disappeared, or an error appears without being announced, the person is stuck. They usually leave.

Webflow's native form handles single-page forms reasonably well. Step logic, step-level validation, and focus management are up to you.

Step 1: Group each step with a fieldset and legend

Each step lives inside a <fieldset> with a <legend>, so screen readers announce the group name before the first field. Every input has a visible <label> connected with for and id, and autocomplete values for personal details (WCAG 2.2 SC 1.3.1 Info and Relationships, SC 1.3.5 Identify Input Purpose).

<!-- Step 1: each step is a fieldset; the legend names the group -->
<fieldset class="gwf-form-step">
  <legend id="step1-heading" tabindex="-1">Step 1 of 3: Your Information</legend>

  <label for="full-name">Full Name</label>
  <input type="text" id="full-name" name="fullName" autocomplete="name" required>

  <label for="email-address">Email Address</label>
  <input type="email" id="email-address" name="email" autocomplete="email" required>
</fieldset>

<!-- One status region for the whole form: announces step changes -->
<p id="form-status" class="sr-only" role="status" aria-live="polite"></p>

‍The sr-only class hides the status text visually while keeping it available to screen readers; use your existing visually-hidden class. The tabindex="-1" on the legend lets the script move focus to it when the step loads, without adding it to the Tab order.

Step 2: Make the step indicator readable, not just visible

The step indicator shows progress visually with colored circles. We checked every color pair against WCAG 2.2 contrast minimums: 4.5:1 for text and 3:1 for the circle edges (SC 1.4.3 Contrast and SC 1.4.11 Non-text Contrast). These are the Book Club's brand colors.

/* Step indicator: brand colors checked for 4.5:1 text contrast */
.gwf-step-circle {
  background-color: #F9D0DE;
  color: #2D2D2D;
  font-size: 2rem;
  font-weight: 800;
}

/* Current step: color changes, but meaning is not carried by color alone */
.gwf-step-item.active .gwf-step-circle {
  background-color: #BF9515;
  color: #2D2D2D;
}

/* Visible focus on every field and button (SC 2.4.7 Focus Visible) */
.gwf-form-step input:focus-visible,
.gwf-form-step textarea:focus-visible,
.gwf-form-step button:focus-visible {
  outline: 3px solid #2D2D2D;
  outline-offset: 2px;
}

‍Color alone can't tell a screen reader user which step they're on. The legend text ("Step 1 of 3") and the status message in Step 4 below carry that meaning.

Step 3: Validate each step and connect errors to fields

Before the form moves forward, the script checks the current step. Each failing field gets a text error placed right after it, aria-invalid="true", and an aria-describedby link to the error, so a screen reader reads the error along with the field. Focus moves to the first field with a problem (SC 3.3.1 Error Identification and SC 3.3.3 Error Suggestion).

// Validate one step. Returns true when every field in the step is valid.
function validateStep(stepIndex) {
  const inputs = steps[stepIndex].querySelectorAll("input, textarea");
  let firstError = null;

  inputs.forEach((input) => {
    const errorId = input.id + "-error";
    let errorEl = document.getElementById(errorId);
    const labelText =
      form.querySelector(`label[for="${input.id}"]`)?.textContent.trim() || "This field";

    if (!input.validity.valid) {
      // Create the error once, right after the field
      if (!errorEl) {
        errorEl = document.createElement("p");
        errorEl.id = errorId;
        errorEl.className = "gwf-error";
        input.after(errorEl);
      }

      // Say what is wrong, not only that something is wrong
      errorEl.textContent = input.validity.typeMismatch && input.type === "email"
        ? "Enter an email address in the format name@example.com."
        : `${labelText} is required.`;

      input.setAttribute("aria-invalid", "true");
      addDescribedBy(input, errorId);
      if (!firstError) firstError = input;
    } else {
      // Clear only this script's error, keep any other descriptions
      if (errorEl) errorEl.remove();
      input.removeAttribute("aria-invalid");
      removeDescribedBy(input, errorId);
    }
  });

  if (firstError) firstError.focus();
  return !firstError;
}

// Add an ID to aria-describedby without overwriting existing hints
function addDescribedBy(el, id) {
  const ids = (el.getAttribute("aria-describedby") || "").split(" ").filter(Boolean);
  if (!ids.includes(id)) ids.push(id);
  el.setAttribute("aria-describedby", ids.join(" "));
}

// Remove one ID from aria-describedby, and drop the attribute if empty
function removeDescribedBy(el, id) {
  const ids = (el.getAttribute("aria-describedby") || "").split(" ").filter((x) => x && x !== id);
  ids.length ? el.setAttribute("aria-describedby", ids.join(" ")) : el.removeAttribute("aria-describedby");
}

Step 4: Move focus and announce every step change

When the step changes, the old "Next" button disappears. If focus stays there, keyboard and screen reader users lose their place (SC 2.4.3 Focus Order). The script moves focus to the new step's legend and updates the status region, so the change is announced once (SC 4.1.3 Status Messages).

// Show one step, move focus to its legend, and announce the change
function goToStep(stepIndex) {
  steps.forEach((step, i) => {
    step.hidden = i !== stepIndex;
  });

  const legend = steps[stepIndex].querySelector("legend");
  if (legend) legend.focus();

  const status = document.getElementById("form-status");
  if (status) status.textContent = `Step ${stepIndex + 1} of ${steps.length}`;
}

‍We don't move focus on page load. Focus only moves in response to something the person did.

The production-only bug: why the review preview went blank

The final step shows a preview of the review before submitting. On the staging domain it worked every time. On the live domain, reading the review field's value from the final step sometimes came back empty.

We didn't chase Webflow's internals. We changed the approach: capture the review text while the person types, then use the saved copy for the preview. The preview has worked in production ever since.

// Save the review text as the person types
let reviewCache = "";
form.querySelector("#book-review").addEventListener("input", (e) => {
  reviewCache = e.target.value.trim();
});

// On the final step, show the saved copy in the preview
const reviewTextEl = document.getElementById("review-text");
if (reviewTextEl) {
  reviewTextEl.textContent = reviewCache;
}

‍The lesson applies well beyond this form. Test every multi-step flow on the published domain, not only on staging. A form that works in one place can still fail in the other.

What this means for your Webflow forms

Most Webflow sites we audit have forms that look finished and still fail people. Errors appear in red with no text explanation, the error isn't tied to its field, or focus is lost between steps. Those failures land on the most valuable pages of a site: quote requests, patient intake, appointment booking.

If your forms feel like that, our post on why Webflow forms fail accessibility covers single-page forms. For multi-step intake and booking forms on healthcare sites, see how we work with healthcare providers.

Frequently asked questions

Are multi-step forms bad for accessibility?

No. A long form split into short, clearly labeled steps can be easier for many people, including people with cognitive disabilities. The risk comes from how steps are built: lost focus, silent errors, and step changes that aren't announced.

Should each step of a multi-step form be a fieldset?

Yes, when a step groups related fields. The <legend> names the group, so screen readers announce it before the first field. A fieldset with a legend doesn't also need aria-labelledby.

Where should focus go when the step changes?

Move it to the new step's heading or legend, and announce the change with a status message. Avoid leaving focus on a button that is now hidden.

Do I need to repeat information people already entered?

No, and you shouldn't ask for it twice. WCAG 2.2 SC 3.3.7 Redundant Entry says information already provided in the same process should be filled in or available to select. A review step that shows what they entered also helps people catch mistakes.

Need help with a complex form?

We build accessible forms into every new site and fix them on existing ones through Webflow accessibility remediation. If you have an intake, quote, or booking form that people aren't finishing, request a free project quote and we'll tell you what's in the way.

Written by Crystal Scott, founder of Graceful Web Studio. Crystal is a Certified Professional in Web Accessibility (CPWA), a Section 508 Trusted Tester, and a Certified Webflow Expert with over a decade of front-end experience.

Other Top Reads for Smarter Websites

Hiring a Website Professional vs. DIY by Graceful Web Studio

DIY website vs hiring a professional: when to make the switch

When to build your own website and when to hire a pro, with a decision checklist covering accessibility, SEO, speed, conversion, and upkeep.

Real SEO Strategies graphic from Graceful Web Studio

How Long Does SEO Take for a Local Business, and What Actually Works?

A realistic local SEO timeline, plus what works: Google Business Profile, an accessible site, and helpful content, with Yakima Valley client examples.

How to Migrate WordPress to Webflow by Graceful Web Studio

How I Migrate My Clients From WordPress to Webflow

Our 10-step process for moving WordPress to Webflow, from sitemap and CSV import to 301 redirects and accessibility testing.

Your Website Deserves Better, Let's Team Up

We would love to meet with you face-to-face. Whether virtually or for a coffee. Request a free project quote, and let’s find the right solution for you! We review your site, set-up a disocvery session and map out goals for a website that grows your business.

Graceful Web Studio powered by Webflow, accessibility and Crystal Scott