AI components

Question

Collect a single choice, multiple choices, or a written answer. Compose several questions into a step-by-step flow.

Installation

pnpm dlx @mielui/svelte add question

Usage

Render Question.Root in the slot Composer.Root would occupy. Keep the prompt value in their shared parent so swapping the two never clears an unsent draft.

<script lang="ts">
    import * as Question from '@mielui/svelte/components/question';

    let {
        onAnswer,
        onSkip
    }: {
        onAnswer: (answer: string) => Promise<void>;
        onSkip: () => void;
    } = $props();

    let answer = $state('');
</script>

<Question.Root variant="inset" bind:value={answer} onSubmit={onAnswer} onCancel={onSkip}>
    <Question.Content>
        <Question.Title>Which environment should I use?</Question.Title>
        <Question.Description>Your prompt draft stays as it is.</Question.Description>
        <Question.Options>
            <Question.Option value="preview" label="Preview" />
            <Question.Option value="production" label="Production" />
        </Question.Options>
    </Question.Content>
    <Question.Actions>
        <Question.Cancel>Skip</Question.Cancel>
        <Question.Submit />
    </Question.Actions>
</Question.Root>

Use type="single" for one option, type="multiple" for several, or type="text" with Question.Input. Single and text modes use a string answer; multiple mode uses a string array. Changing type clears the answer to the new mode's empty value.

Question waits for an async onSubmit and ignores repeat submissions while it is pending. A rejected submission keeps the answer and shows the error message until the next attempt. Use onError to report failures. Changing mode or unmounting ignores an unfinished submission's result.

Options use the same indicators as Checkbox and RadioGroup: square checkboxes in multiple mode and round radios otherwise. An error shows one message above the frame and tints the frame edge.

Composition

variant="inset" frames the question with a recessed content surface; the default variant uses a plain Card. Place Question.Actions after Question.Content to keep the controls in the footer. Both the description and the actions are optional.

Question.Content groups the title, description, and answer controls in a fieldset. It does not manage step navigation. Store the step index and answers in the parent. When a flow mixes answer types, key Root by question so each step gets its own state.

To use Cancel as a Back button, call event.preventDefault() in its onclick before navigating. This keeps Root's onCancel from running.

<Question.Root
    variant="inset"
    bind:value={answers[step]}
    labels={{ submit: 'Next' }}
    onSubmit={next}
>
    <Question.Content>
        <Question.Title>{question.title}</Question.Title>
        <Question.Options>
            {#each question.options as option (option.value)}
                <Question.Option {...option} />
            {/each}
        </Question.Options>
    </Question.Content>
    <Question.Actions>
        <Question.Cancel
            disabled={step === 0}
            onclick={(event) => {
                event.preventDefault();
                step -= 1;
            }}
        >
            Back
        </Question.Cancel>
        <Question.Submit />
    </Question.Actions>
</Question.Root>

Examples

Question series

Three questions in a row with a back button, a step count, and a summary at the end.

Multiple choice

Free text

Composer takeover

Answer or skip the question to restore the composer with its draft intact.

Translated labels

Submit without choosing to see the translated validation message.

Text answers

Question.Input is a textarea that grows as the person types. rows sets its starting height, 2 by default, and autoresize={false} keeps that height and scrolls instead. Enter submits and Shift+Enter adds a line; submitOnEnter={false} makes Enter add a line. readonly shows the answer without allowing edits.

In a question with options, pressing 1 to 9 picks the option with that number, and each option shows its key. Digits typed into a text answer are left alone.

Parts

Question.Submit reads labels.submit for its text and labels.submitting for the text it shows while onSubmit is pending; pass children to replace its content. Question.Input takes placeholder and aria-label, which win over labels.placeholder and labels.answer. Submit, Input and Option accept bind:element for their DOM node.

Root, Content, Title, Description, Options, Actions, Cancel and Submit render their children and accept class and other element attributes.

Labels

Built-in text is English by default. Pass a labels object of type QuestionLabels to Question.Root to translate the three validation messages, the message shown when submitting fails, the submit button text, and the answer field's placeholder and accessible name. Every key is optional; omitted keys keep their default.

<script lang="ts">
    import * as Question from '@mielui/svelte/components/question';
    import type { QuestionLabels } from '@mielui/svelte/components/question';

    const labels: QuestionLabels = {
        selectAnswer: 'Choisissez une rรฉponse.',
        selectAnswers: 'Choisissez au moins une rรฉponse.',
        enterAnswer: 'Saisissez une rรฉponse.',
        submitError: "La rรฉponse n'a pas pu รชtre envoyรฉe.",
        submit: 'Envoyer',
        submitting: 'Envoiโ€ฆ',
        answer: 'Rรฉponse',
        placeholder: 'Saisissez votre rรฉponseโ€ฆ'
    };
</script>

API reference

Question.Root

Renders a full question form.

  • labels
    QuestionLabels

    Overrides the built-in text and accessible names. Every key is optional and English is the fallback.

  • variant
    "default" | "inset"

    inset frames the question with its actions on the frame.

    Default 'default'

  • status
    QuestionStatus

    Current state, which drives the submit control.

    Default 'idle'

  • disabled
    boolean

    Prevents interaction and dims the control.

    Default false

  • required
    boolean

    Requires an answer before submitting.

    Default true

  • autofocus
    boolean

    Focuses the first control when it mounts.

    Default false

  • name
    string

    Field name of the answer.

    Default 'answer'

  • onError
    ((error: unknown) => void)

    Called with the error when onSubmit rejects.

  • onCancel
    ((event: MouseEvent) => void)

    Called when Cancel is pressed.

  • class
    string

    Classes added to the element.

  • children
    Snippet<[]>

    Content rendered inside.

  • type
    "single" | "text" | "multiple"

    single and multiple offer options. text takes a written answer.

    Default 'single'

  • value Bindable
    string | string[]

    Current answer. Bindable.

  • onSubmit Required
    (answer: string, event: SubmitEvent) => void | Promise<void> | (answer: string[], event: SubmitEvent) => void | Promise<void>

    Called with the answer. Return a promise to show progress until it settles.

Question.Actions

Groups cancellation and submit actions.

  • class
    string

    Classes added to the element.

  • children
    Snippet<[]>

    Content rendered inside.

Question.Cancel

Cancels question submission.

  • children
    Snippet<[]>

    Content rendered inside.

  • onclick
    ((event: MouseEvent) => void)

    Called when it is clicked or activated.

Question.Content

  • class
    string

    Classes added to the element.

  • children
    Snippet<[]>

    Content rendered inside.

Question.Description

Explains the question context.

  • class
    string

    Classes added to the element.

  • children
    Snippet<[]>

    Content rendered inside.

Question.Input

Collects a free-form response.

  • submitOnEnter
    boolean

    Enter submits and Shift+Enter adds a line. Set false to make Enter add a line.

    Default true

  • autoresize
    boolean

    Grows with its content instead of scrolling.

    Default true

  • placeholder
    string

    Text shown while there is no value.

  • aria-label
    string

    Accessible name of the answer field.

  • rows
    number

    Starting height in text rows.

    Default 2

  • disabled
    boolean

    Prevents interaction and dims the control.

    Default false

  • readonly
    boolean

    Shows the value without allowing edits. The value is still submitted.

    Default false

  • element Bindable
    HTMLTextAreaElement

    Bindable reference to the DOM element.

  • class
    string

    Classes added to the element.

Question.Option

Represents a question option.

  • value Required
    string

    Value this option answers with.

  • label Required
    string

    Visible label of the option.

  • description
    string

    Supporting text under the option label.

  • disabled
    boolean

    Prevents interaction and dims the control.

    Default false

  • element Bindable
    HTMLInputElement

    Bindable reference to the DOM element.

  • class
    string

    Classes added to the element.

Question.Options

Wraps answer option items.

  • class
    string

    Classes added to the element.

  • children
    Snippet<[]>

    Content rendered inside.

Question.Submit

Submits the selected answer.

  • children
    Snippet<[]>

    Content rendered inside.

  • element Bindable
    HTMLButtonElement | HTMLAnchorElement

    Bindable reference to the DOM element.

Question.Title

Shows the question heading.

  • class
    string

    Classes added to the element.

  • children
    Snippet<[]>

    Content rendered inside.