Put each question in a <fieldset> inside a
<form class="questionnaire">, and add a
<footer> with the buttons that move between them.
The form asks one question at a time.
Use the <legend> for the prompt and a
<p> under it to describe the question. Answers are
ordinary radio buttons,
checkboxes or
text fields. A
<small> inside a choice's label describes that
choice. Mark the footer buttons by their value:
previous, skip and next,
plus a submit button. The component adds the progress row and
shows only the question and buttons that apply.
Use a questionnaire for a short survey, onboarding or intake form where each question deserves the whole screen. For a long form people need to scan and correct, keep every field on the page. The stepper suits a few large stages, such as a checkout, each with several fields.
<form class="questionnaire" aria-label="Product survey">
<fieldset>
<legend>What should we prototype next?</legend>
<p>Pick the direction you want to explore first.</p>
<label><input type="radio" name="direction" value="search" required>Search<small>Find any page in two keystrokes</small></label>
<label><input type="radio" name="direction" value="sharing" required>Sharing<small>Send a view to a teammate</small></label>
<p class="questionnaire-error">Choose an answer to continue.</p>
</fieldset>
<fieldset data-required>
<legend>Where do you use the app?</legend>
<label><input type="checkbox" name="platforms" value="web">Web</label>
<label><input type="checkbox" name="platforms" value="mobile">Mobile</label>
<p class="questionnaire-error">Choose at least one platform.</p>
</fieldset>
<fieldset>
<legend>Anything else we should know?</legend>
<div class="field">
<textarea id="notes" name="notes" class="expressive-textarea" placeholder=" "></textarea>
<label for="notes">Notes</label>
</div>
</fieldset>
<footer>
<button type="button" class="text" value="previous">Back</button>
<button type="button" class="text" value="skip">Skip</button>
<button type="button" value="next">Next</button>
<button type="submit">Submit</button>
</footer>
</form>
Questions
The answers decide the kind of question. Radio buttons with one
name make a single choice, and checkboxes make a
multiple choice. A text field or textarea takes a free answer, and a
question can mix choices with a text field for "Other".
Add required to one of a question's controls to make
the question required. HTML has no way for a group of checkboxes to
say "at least one", so put data-required on its fieldset
instead. Any other question is optional, and the Skip button appears
for it. Skipping clears whatever was answered, so the form sends
nothing for that question. Leave out the Skip button to make people
press Next instead.
Validation
Next checks the shown question with the browser's own validation, so
required, pattern, minlength
and the input types all work. When the answer is missing or wrong,
the fieldset gets invalid and its controls get
aria-invalid="true". Its
.questionnaire-error shows, and focus moves to the
control to fix. Answering clears the error.
Write the error text in the question so it can be specific and in
the page's language. A question without one shows the browser's
validation message, or the requiredMessage option for a
data-required group. Submitting checks every question
and opens the first one that still needs an answer, whether it comes
from the button, Enter or requestSubmit(). Only enabled
checkboxes count toward data-required, since the browser
does not send a disabled one.
Conditional questions
To branch, enable or disable questions based on earlier answers. The component skips a disabled fieldset, and the progress count leaves it out. The browser does not send the controls of a disabled fieldset.
<fieldset>
<legend>Do you work in a team?</legend>
<label><input type="radio" name="team" value="yes" required>Yes</label>
<label><input type="radio" name="team" value="no" required>No</label>
</fieldset>
<fieldset id="team-size" disabled>
<legend>How many people are on your team?</legend>
…
</fieldset>
<script>
form.addEventListener('change', () => {
document.getElementById('team-size').disabled = form.elements.team.value !== 'yes';
});
</script>
Submitting
The submit button shows on the last question. Pressing it sends the
form like any other, to its action or to a
submit listener that reads the answers with
FormData. Use getAll() for checkboxes.
Without JavaScript, every question shows at once, the navigation
buttons stay hidden, and the submit button still sends the form.
In that case nothing checks a data-required group.
form.addEventListener('submit', (event) => {
event.preventDefault();
const answers = new FormData(event.currentTarget);
answers.get('direction'); // 'search'
answers.getAll('platforms'); // ['web', 'mobile']
});
Shortcuts, review and saving
Add any of these three attributes to the form:
-
data-shortcutslets a letter or a digit pick the answer at that place in the shown question, A or 1 for the first. It ends each choice with its letter, which screen readers skip. Keys typed into a text field stay text. -
data-summaryadds a review step after the last question. It lists every question with its answer and an Edit button back to it. The submit button shows there instead. -
data-save="key"keeps the answers and the shown question inlocalStorageunder that key. It restores them when the page comes back and forgets them once the form is submitted. A restored answer fireschange, so conditional questions open as they did. File inputs are not saved. Keep personal answers out of saved questionnaires on shared devices.
<form class="questionnaire" aria-label="Shortcut survey" data-shortcuts data-summary data-save="survey">
<fieldset>
<legend>How did you hear about us?</legend>
<label><input type="radio" name="heard" value="search" required>Search engine</label>
<label><input type="radio" name="heard" value="friend" required>A friend</label>
<label><input type="radio" name="heard" value="ad" required>An ad</label>
</fieldset>
<!-- More questions -->
<footer>
<button type="button" class="text" value="previous">Back</button>
<button type="button" class="text" value="skip">Skip</button>
<button type="button" value="next">Next</button>
<button type="submit">Submit</button>
</footer>
</form>
Accessibility
Each question is a native group named by its legend. The component
attaches the description and any error with
aria-describedby. Hidden questions use
hidden, so they are out of the tab order and the
accessibility tree. After Next, Back or Skip, focus moves to the
selected answer or the first control of the new question.
Name the form with aria-label or a heading. Enter in a
field moves to the next question, and on the last one it submits.
The progress row is a <progress> named by its
text, "Question 2 of 3".
Options
| Name | Type | Default | Description |
|---|---|---|---|
progressLabel |
String | 'Question {current} of {total}' |
Text of the progress row. {current} and {total} become the question number and count. data-progress-label on the form sets it. |
requiredMessage |
String | 'Choose an answer to continue.' |
Error for a question with no .questionnaire-error when the browser has no message, as for a data-required group. |
summaryLabel |
String | 'Review your answers' |
With data-summary, the review step's name and the text of the progress row there. |
editLabel |
String | 'Edit' |
With data-summary, the text of each answer's button back to its question. |
skippedLabel |
String | 'Not answered' |
With data-summary, the answer shown for a question left blank. |
Methods
const questionnaire = Expressive.Questionnaire.getInstance(document.querySelector('form.questionnaire'));
questionnaire.items; // the fieldsets that apply, in order
questionnaire.index; // position of the shown question
questionnaire.next(); // checks the shown question, then moves on
questionnaire.previous();
questionnaire.skip(); // clears the shown answer, then moves on
questionnaire.goTo(2); // shows a question without checking, e.g. to resume
questionnaire.review(); // with data-summary, shows the review step
questionnaire.destroy(); // shows every question and removes what it added
Tokens
Override these on the .questionnaire.
| Token | Default |
|---|---|
--md-comp-questionnaire-choice-shape | 12px |
--md-comp-questionnaire-choice-outline-color | --md-sys-color-outline-variant |
--md-comp-questionnaire-choice-selected-color | --md-sys-color-secondary-container |