Skip to content

Repository files navigation

A11y Conditional Fields

Framework-agnostic progressive enhancement for accessible conditional form sections.

Installation

This package is configured for npm publishing. Before sharing the npm install commands publicly, confirm the package has been published under the configured name.

npm install a11y-conditional-fields
pnpm add a11y-conditional-fields
yarn add a11y-conditional-fields

Usage

import { createConditionalFields } from "a11y-conditional-fields";

const form = document.querySelector("[data-a11y-conditional-fields]");

if (form instanceof HTMLFormElement) {
  const conditionalFields = createConditionalFields(form, {
    announceChanges: true,
    disableWhenHidden: true
  });

  conditionalFields.refresh();
}

The package is ESM-only and does not auto-initialize on import.

Distribution Files

The default package entry points to the readable ESM build at dist/index.js. npm run build also emits a minified runtime bundle at dist/index.min.js for static-site, CDN, and other pre-minified ESM use cases.

Most app bundlers should use the default import:

import { createConditionalFields } from "a11y-conditional-fields";

Use the minified subpath when you specifically need the pre-minified runtime:

import { createConditionalFields } from "a11y-conditional-fields/min";

The ./min subpath exposes the same public API and reuses the same dist/index.d.ts declarations. Docs metadata remains available from a11y-conditional-fields/docs.

HTML Structure

Use a form root, unique ids on conditional sections, and trigger attributes that point to those ids. Multiple target ids can be separated with spaces.

<form data-a11y-conditional-fields>
  <label>
    <input type="checkbox" data-condition-toggle="shipping-address delivery-notes">
    Use a different shipping address
  </label>

  <section id="shipping-address" data-condition-target hidden aria-labelledby="shipping-title">
    <h2 id="shipping-title">Shipping address</h2>
    <label for="street">Street</label>
    <input id="street" name="street" data-required-when-visible>
  </section>

  <section id="delivery-notes" data-condition-target hidden aria-labelledby="delivery-title">
    <h2 id="delivery-title">Delivery notes</h2>
    <label for="notes">Notes for the courier</label>
    <textarea id="notes" name="notes"></textarea>
  </section>

  <p data-condition-status></p>
</form>

Supported triggers:

  • input[type="checkbox"][data-condition-toggle]
  • button[data-condition-toggle]
  • input[type="radio"][data-condition-show]
  • select[data-condition-select] with option-level data-condition-show

Useful data attributes:

  • data-condition-target marks a hideable section. Give every target a unique id.
  • data-required-when-visible makes a field required only while its section is visible.
  • data-disable-when-hidden opts a field into disabled-when-hidden behavior.
  • data-condition-preserve prevents automatic disable or clear behavior for that field. Preserved named controls can remain submit-eligible while hidden.
  • data-clear-when-hidden clears a field when its section hides.
  • data-focus-first on a target or data-focus-when-shown on a field opts into focus movement.
  • data-condition-status provides a live region for show/hide announcements.
  • data-error marks validation text that should be hidden and disconnected when a target hides.

API

import {
  A11yConditionalFields,
  createConditionalFields,
  initConditionalFieldsAll
} from "a11y-conditional-fields";
  • createConditionalFields(form, options) initializes one form and returns a ConditionalFieldsInstance.
  • initConditionalFieldsAll(options, root) initializes every form[data-a11y-conditional-fields] in a document or subtree.
  • new A11yConditionalFields(form, options) is available for class-based integration.
  • refresh() rescans dynamic targets and triggers.
  • show(targetId), hide(targetId), toggle(targetId, visible), and isVisible(targetId) manage known targets by id.
  • destroy() removes listeners, restores managed attributes and field state, and allows the form to be initialized again.

Options:

Option Default Description
clearWhenHidden false Clears managed fields when their section hides, unless the field has data-condition-preserve.
disableWhenHidden true Disables managed controls while hidden so they are skipped by keyboard and form submission.
announceChanges true Uses a polite status region for show/hide messages.
focusFirstOnShow false Moves focus to the first focusable field when a target opens.
validateOnHide false Calls checkValidity() after fields are hidden.
collapseOnEscape false Lets Escape hide a visible controlled section that contains focus.

The same boolean options can be set as data attributes on the form, such as data-clear-when-hidden="true" or data-collapse-on-escape="true". JavaScript options take precedence.

Submission Behavior

By default, hidden managed fields are disabled before submission. Native form submission and new FormData(form) skip disabled controls, so hidden section values are not sent unless you opt out of that behavior.

Hiding a section does not clear values by default. The values stay in the DOM and are available again if the section is shown later. Use clearWhenHidden or data-clear-when-hidden when stale hidden values should be erased.

If you set disableWhenHidden: false or add data-condition-preserve to a field, that named control can stay enabled while hidden. Treat that as an application data decision: filter hidden values before submit, or intentionally accept them in your server-side handling.

Keyboard Behavior

Key or action Behavior
Tab / Shift+Tab Follows the native form order. Hidden conditional fields are not focusable.
Enter / Space on a checkbox or button trigger Uses native control behavior to update the target section.
Arrow keys in radio groups or selects Uses native browser behavior for the form control.
Escape inside an open target When collapseOnEscape is enabled, hides that target and restores focus to its controlling trigger.

Accessibility Notes

The plugin starts from semantic form markup. It keeps hidden sections out of keyboard and assistive-technology flows with hidden, inert, aria-hidden, and disabled form controls. It adds aria-controls and aria-expanded to checkbox, radio, and button triggers, and it uses a polite role="status" live region when announcements are enabled.

Use data-required-when-visible for fields that should only be required while their section is visible. Existing validation errors marked with data-error are hidden and disconnected from aria-describedby or aria-errormessage when their section hides. Keep labels or legends near the controls that reveal conditional fields.

Focus stays on the current control by default. Add data-focus-first to a target or data-focus-when-shown to a specific field only when moving focus reduces effort. When collapseOnEscape is enabled, pressing Escape inside a visible conditional target hides that target and restores focus to its controlling trigger.

Still test the final form with the browsers and assistive technologies your audience uses. The plugin manages section state; it does not prove that the surrounding form copy, validation flow, or data model is accessible.

Lifecycle Events

Lifecycle events bubble from the form and include the plugin instance in event.detail.instance.

  • a11y-conditional-fields:init
  • a11y-conditional-fields:refresh
  • a11y-conditional-fields:show
  • a11y-conditional-fields:hide
  • a11y-conditional-fields:change
  • a11y-conditional-fields:destroy

Show, hide, and change events also include targetId, target, trigger, and visible.

Limitations

  • The plugin does not create form markup for you.
  • Targets without an id are ignored because triggers need stable references.
  • It does not provide an error summary or custom validation UI.
  • It does not add custom arrow-key behavior to native form controls.
  • Live-region output is intentionally short and still needs manual screen reader verification.
  • There is no CSS export; bring your own form and focus styles.

Examples

  • examples/basic shows a semantic form using checkbox, radio, select, default focus retention, Escape focus restoration, and live-region patterns.
  • examples/options-lab compares the main options, including focusFirstOnShow, collapseOnEscape, clearWhenHidden, validateOnHide, announceChanges, and disableWhenHidden.
  • examples/form-validator-integration combines conditional fields with a11y-form-validator so hidden sections do not block validation while visible follow-up fields still receive summaries and inline errors.

Build the package before opening an example because examples import from ../../dist/index.js.

npm run build
python3 -m http.server 4173

Then open one of these URLs:

  • http://127.0.0.1:4173/examples/basic/
  • http://127.0.0.1:4173/examples/options-lab/
  • http://127.0.0.1:4173/examples/form-validator-integration/

GitHub Pages Demo

The root index.html is a static demo and compact documentation page. Build the package and assemble the Pages artifact with:

npm run build:pages

The generated _site/ folder contains index.html, the example pages, dist/, the validator demo assets, and .nojekyll. Configure GitHub Pages to use GitHub Actions. After deployment, the project page is expected at:

https://vmitsaras.github.io/a11y-conditional-fields/

Docs Metadata

import { docs } from "a11y-conditional-fields/docs";

License

MIT

About

A TypeScript plugin for accessible conditional form sections, covering native triggers, FormData-safe hidden fields, Escape collapse, and status updates.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages