Questionnaire
Asks the reader a question and collects the answer as a set of large, tappable choices
<form method="post">
<sa-questionnaire>
<sa-questionnaire-item asp-for="TravelStyle" required="true">
<sa-questionnaire-title>How would you like to travel?</sa-questionnaire-title>
<sa-questionnaire-description>
We will shape the rest of your itinerary around this.
</sa-questionnaire-description>
<sa-questionnaire-choices>
<sa-questionnaire-choice value="direct">
Direct flights only
<sa-questionnaire-choice-description>
Fewest connections, usually the highest fare.
</sa-questionnaire-choice-description>
</sa-questionnaire-choice>
<sa-questionnaire-choice value="one-stop">
One stop is fine
<sa-questionnaire-choice-description>
A good balance of price and travel time.
</sa-questionnaire-choice-description>
</sa-questionnaire-choice>
<sa-questionnaire-choice value="cheapest">
Cheapest route, any stops
<sa-questionnaire-choice-description>
Longer journeys in exchange for the lowest fare.
</sa-questionnaire-choice-description>
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
</sa-questionnaire>
</form>Usage
A questionnaire is <sa-questionnaire> wrapping one or more <sa-questionnaire-item>, each of which is a single question: a title, an optional description, and a set of <sa-questionnaire-choice> inside <sa-questionnaire-choices>.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-progress current="..." total="..."/>
<sa-questionnaire-item asp-for="..." required="..." multiple="...">
<sa-questionnaire-title>...</sa-questionnaire-title>
<sa-questionnaire-description>...</sa-questionnaire-description>
<sa-questionnaire-choices shortcuts="...">
<sa-questionnaire-choice value="...">
...
<sa-questionnaire-choice-description>...</sa-questionnaire-choice-description>
</sa-questionnaire-choice>
<sa-questionnaire-input asp-for="..."/>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-actions>
<sa-questionnaire-previous/>
<sa-questionnaire-skip/>
<sa-questionnaire-next/>
</sa-questionnaire-actions>
</sa-questionnaire>
</form>Each <sa-questionnaire-item> renders a <fieldset> with its title as the <legend>, and each choice is a real radio button or checkbox, so a questionnaire posts and validates like any other form.
Bind the question, not its choices. asp-for goes on <sa-questionnaire-item>, and everything inside it follows from there: the choices take their name and selected state from it, and its validation message reports it. Each choice supplies only the value it posts. A question with no binding works too — put a name on the item instead, and the choices post under that.
The component holds no client-side state. It renders whatever the page model gives it and posts back on submit, so a multi-step questionnaire is a sequence of requests that your page model drives, not a wizard running in the browser.
Examples
Multiple answers
Set multiple on the item to let the reader pick more than one answer. Its choices render as checkboxes rather than radio buttons, so bind the item to a collection property.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-item asp-for="Extras" multiple="true">
<sa-questionnaire-title>What should we add to your booking?</sa-questionnaire-title>
<sa-questionnaire-description>
Select everything you would like included. You can remove extras later.
</sa-questionnaire-description>
<sa-questionnaire-choices>
<sa-questionnaire-choice value="transfers">
Airport transfers
</sa-questionnaire-choice>
<sa-questionnaire-choice value="insurance">
Travel insurance
</sa-questionnaire-choice>
<sa-questionnaire-choice value="tours">
Guided day tours
</sa-questionnaire-choice>
<sa-questionnaire-choice value="lounge">
Airport lounge access
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
</sa-questionnaire>
</form>Free-text answer
<sa-questionnaire-input> adds a free-text answer alongside the fixed choices, for the reader whose answer is not on the list. It binds to a property of its own — it is a different answer from the one the choices post — so give it its own asp-for.
Always give it an accessible name with a visible label, aria-label or aria-labelledby. A placeholder is not a name: it disappears as soon as the reader types.
A question that takes one answer takes it from one place, so the free text and the choices stand in for each other: typing here clears the selected choice, and selecting a choice takes the typed answer back out of the form. Only one of the two ever posts, and what the reader typed is never thrown away — it stays on the page to come back to, out of the form until it is the answer again. An empty free-text answer is out of the form too, so an untouched one binds as null rather than an empty string.
Set replaces-choices="false" to turn that off and let both answers stand and post together. It is off already on a multiple question, where free text adds to the answer rather than replacing it.
All of this needs stellar-admin.js. Without it both answers post and your handler decides between them.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-item asp-for="Destination">
<sa-questionnaire-title>Where would you like to go next?</sa-questionnaire-title>
<sa-questionnaire-description>
Pick one of our most requested trips, or tell us somewhere else.
</sa-questionnaire-description>
<sa-questionnaire-choices>
<sa-questionnaire-choice value="kyoto">
Kyoto, Japan
</sa-questionnaire-choice>
<sa-questionnaire-choice value="patagonia">
Patagonia, Chile
</sa-questionnaire-choice>
<sa-questionnaire-choice value="lisbon">
Lisbon, Portugal
</sa-questionnaire-choice>
<sa-questionnaire-input asp-for="OtherDestination"
aria-label="Somewhere else"
placeholder="Somewhere else..."/>
</sa-questionnaire-choices>
</sa-questionnaire-item>
</sa-questionnaire>
</form>Validation
A bound question renders its own validation message, so there is nothing to remember: bind the item and an invalid answer reports itself. The message is rendered even while the answer is valid — hidden until it says something — which is what lets client-side validation write into it without the layout shifting.
A free-text answer reports separately, next to the input, because it is bound to its own property. In the example below the choices are fine and the typed answer is too short, so only the input's message shows.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-item asp-for="CabinClass" required="true">
<sa-questionnaire-title>Which cabin class should we book?</sa-questionnaire-title>
<sa-questionnaire-description>
Fares are held for 20 minutes once you continue.
</sa-questionnaire-description>
<sa-questionnaire-choices>
<sa-questionnaire-choice value="economy">
Economy
</sa-questionnaire-choice>
<sa-questionnaire-choice value="premium">
Premium economy
</sa-questionnaire-choice>
<sa-questionnaire-choice value="business">
Business
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-item asp-for="Occasion">
<sa-questionnaire-title>What is the trip built around?</sa-questionnaire-title>
<sa-questionnaire-description>
We time the itinerary around whatever you are travelling for.
</sa-questionnaire-description>
<sa-questionnaire-choices>
<sa-questionnaire-choice value="festival">
A festival or event
</sa-questionnaire-choice>
<sa-questionnaire-choice value="wedding">
A wedding
</sa-questionnaire-choice>
<sa-questionnaire-choice value="birthday">
A big birthday
</sa-questionnaire-choice>
<sa-questionnaire-input asp-for="OtherOccasion"
aria-label="Something else"
placeholder="Something else..."/>
</sa-questionnaire-choices>
</sa-questionnaire-item>
</sa-questionnaire>
</form>Place <sa-questionnaire-error> yourself when you want the message somewhere other than the end of the question, or want to write your own text. Doing so replaces the automatic message rather than adding a second one. To leave the message out altogether, set render-error="false" on the item.
Shortcut keys
Set shortcuts on <sa-questionnaire-choices> to label each choice with a key that selects it. QuestionnaireShortcuts.Letters uses A through Z and QuestionnaireShortcuts.Numbers 1 through 9. A choice can override the key with shortcut.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-item asp-for="Departure" required="true">
<sa-questionnaire-title>When would you like to depart?</sa-questionnaire-title>
<sa-questionnaire-description>
Departure dates change the fare more than anything else.
</sa-questionnaire-description>
<sa-questionnaire-choices shortcuts="QuestionnaireShortcuts.Letters">
<sa-questionnaire-choice value="asap">
As soon as possible
</sa-questionnaire-choice>
<sa-questionnaire-choice value="shoulder">
Shoulder season
</sa-questionnaire-choice>
<sa-questionnaire-choice value="flexible">
I am flexible
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
</sa-questionnaire>
</form>The badge is decorative and hidden from assistive technology; the key is announced from the control it operates, through aria-keyshortcuts on the input.
Steps
Pair <sa-questionnaire-progress> with an actions row to present one question at a time. Give the progress bar current and total and it reports position both visually and to assistive technology; supply your own content instead to replace the wording.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-progress current="2" total="4"/>
<sa-questionnaire-item asp-for="Pace" required="true">
<sa-questionnaire-title>What pace suits this trip?</sa-questionnaire-title>
<sa-questionnaire-description>
You can still adjust individual days once the itinerary is drafted.
</sa-questionnaire-description>
<sa-questionnaire-choices>
<sa-questionnaire-choice value="relaxed">
Relaxed
<sa-questionnaire-choice-description>
Two or three plans a day, with time to wander.
</sa-questionnaire-choice-description>
</sa-questionnaire-choice>
<sa-questionnaire-choice value="balanced">
Balanced
<sa-questionnaire-choice-description>
A full morning and afternoon, evenings left open.
</sa-questionnaire-choice-description>
</sa-questionnaire-choice>
<sa-questionnaire-choice value="packed">
Packed
<sa-questionnaire-choice-description>
See as much as possible, early starts included.
</sa-questionnaire-choice-description>
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-actions>
<sa-questionnaire-previous name="step" value="back"/>
<sa-questionnaire-skip name="step" value="skip"/>
<sa-questionnaire-next name="step" value="next"/>
</sa-questionnaire-actions>
</sa-questionnaire>
</form>The actions render submit buttons. Give them a shared name and distinct value attributes to identify which one was clicked on the server.
Custom progress
Give <sa-questionnaire-progress> content of your own to replace the default wording — a row of segments, a step label, whatever suits the flow. Keep current and total on it so it still reports as a progress bar, and set aria-valuetext to match your custom text.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-progress current="@Model.Current"
total="@Model.Total"
aria-valuetext="Step @Model.Current of @Model.Total"
class="flex w-full flex-col gap-2">
<span class="flex gap-1.5" aria-hidden="true">
@for (var step = 1; step <= Model.Total; step++)
{
<span class="h-1.5 flex-1 rounded-full @(step <= Model.Current ? "bg-primary" : "bg-muted")"></span>
}
</span>
<span>Step @Model.Current of @Model.Total</span>
</sa-questionnaire-progress>
<sa-questionnaire-item asp-for="Travellers" required="true">
<sa-questionnaire-title>Who is travelling?</sa-questionnaire-title>
<sa-questionnaire-description>
We hold seats together wherever the airline allows it.
</sa-questionnaire-description>
<sa-questionnaire-choices>
<sa-questionnaire-choice value="solo">
Just me
</sa-questionnaire-choice>
<sa-questionnaire-choice value="couple">
Two of us
</sa-questionnaire-choice>
<sa-questionnaire-choice value="family">
A family
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-actions>
<sa-questionnaire-previous name="step" value="back"/>
<sa-questionnaire-next name="step" value="next"/>
</sa-questionnaire-actions>
</sa-questionnaire>
</form>In a card
A questionnaire drops into <sa-card-content> unchanged, which is the usual way to present a step of a longer flow.
<sa-card>
<sa-card-content>
<form method="post">
<sa-questionnaire>
<sa-questionnaire-progress current="3" total="5"/>
<sa-questionnaire-item asp-for="SeatPreference" required="true">
<sa-questionnaire-title>Where would you like to sit?</sa-questionnaire-title>
<sa-questionnaire-description>
We will request this seat on every flight in your itinerary.
</sa-questionnaire-description>
<sa-questionnaire-choices shortcuts="QuestionnaireShortcuts.Numbers">
<sa-questionnaire-choice value="window">
Window
</sa-questionnaire-choice>
<sa-questionnaire-choice value="aisle">
Aisle
</sa-questionnaire-choice>
<sa-questionnaire-choice value="any">
No preference
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-actions>
<sa-questionnaire-previous name="step" value="back"/>
<sa-questionnaire-next name="step" value="next"/>
</sa-questionnaire-actions>
</sa-questionnaire>
</form>
</sa-card-content>
</sa-card>Long form
Several questions can share one questionnaire and post together, which suits a preferences page better than stepping through them one at a time. Each question restarts its own shortcut keys, and a key answers the question it was pressed in.
<form method="post">
<sa-questionnaire>
<sa-questionnaire-item asp-for="Budget" required="true">
<sa-questionnaire-title>What is your budget per person?</sa-questionnaire-title>
<sa-questionnaire-choices shortcuts="QuestionnaireShortcuts.Letters">
<sa-questionnaire-choice value="under-2000">
Under $2,000
</sa-questionnaire-choice>
<sa-questionnaire-choice value="2000-5000">
$2,000 to $5,000
</sa-questionnaire-choice>
<sa-questionnaire-choice value="over-5000">
Over $5,000
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-item asp-for="Accommodation" required="true">
<sa-questionnaire-title>Where would you like to stay?</sa-questionnaire-title>
<sa-questionnaire-choices shortcuts="QuestionnaireShortcuts.Letters">
<sa-questionnaire-choice value="hotel">
International hotels
</sa-questionnaire-choice>
<sa-questionnaire-choice value="boutique">
Boutique guesthouses
</sa-questionnaire-choice>
<sa-questionnaire-choice value="apartment">
Self-catering apartments
</sa-questionnaire-choice>
<sa-questionnaire-input asp-for="OtherAccommodation"
aria-label="Something else"
placeholder="Something else..."/>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-item asp-for="Interests" multiple="true">
<sa-questionnaire-title>What would you like to build the trip around?</sa-questionnaire-title>
<sa-questionnaire-choices shortcuts="QuestionnaireShortcuts.Letters">
<sa-questionnaire-choice value="food">
Food and markets
</sa-questionnaire-choice>
<sa-questionnaire-choice value="history">
History and museums
</sa-questionnaire-choice>
<sa-questionnaire-choice value="outdoors">
Hiking and the outdoors
</sa-questionnaire-choice>
</sa-questionnaire-choices>
</sa-questionnaire-item>
<sa-questionnaire-actions>
<sa-questionnaire-submit>Save preferences</sa-questionnaire-submit>
</sa-questionnaire-actions>
</sa-questionnaire>
</form>API Reference
<sa-questionnaire>
Renders a <sel-questionnaire> custom element holding the questions, forwarding any global attributes. It handles the shortcut keys assigned by <sa-questionnaire-choices>, the arrow keys that move between a question's answers, and keeping a free-text answer and the choices from both posting — all of which needs stellar-admin.js.
<sa-questionnaire-progress>
Renders a <div> showing how far through the questionnaire the reader is. Supply both current and total to render a progress bar, or supply your own content to replace the default wording.
Prop
Type
<sa-questionnaire-item>
Renders a <fieldset> holding one question, forwarding any global attributes.
Prop
Type
<sa-questionnaire-title>
Renders the item's <legend>, naming the question the item asks.
<sa-questionnaire-description>
Renders a <div> holding supporting text for the question.
<sa-questionnaire-choices>
Renders a <div> holding the question's answers, forwarding any global attributes.
Prop
Type
<sa-questionnaire-choice>
Renders a <div> wrapping a native radio button — or a checkbox when the question accepts multiple answers — together with its label.
Prop
Type
<sa-questionnaire-choice-description>
Renders a <span> holding supporting text for a single choice.
<sa-questionnaire-input>
Renders a free-text <input> alongside the question's fixed choices, forwarding any global attributes. It binds to a property of its own rather than the one the choices post.
Prop
Type
<sa-questionnaire-error>
Renders a <div> holding the answer's validation message. A bound question renders one automatically, so place this only to position the message yourself or to write your own text — an explicit one replaces the automatic message rather than adding to it.
<sa-questionnaire-actions>
Renders a <div> holding the questionnaire's navigation buttons.
<sa-questionnaire-previous>
Renders a submit <button> that moves back to the previous question.
Prop
Type
<sa-questionnaire-skip>
Renders a submit <button> that leaves the current question unanswered and moves on. Takes the same size and variant attributes as <sa-questionnaire-previous>, and defaults to ButtonVariant.Outline.
<sa-questionnaire-next>
Renders a submit <button> that moves on to the next question. Takes the same size and variant attributes as <sa-questionnaire-previous>, and defaults to ButtonVariant.Default.
<sa-questionnaire-submit>
Renders a submit <button> that completes the questionnaire. Takes the same size and variant attributes as <sa-questionnaire-previous>, and defaults to ButtonVariant.Default.