# Question

Inline agent question form with single-choice, multiple-choice, and free-text answers.

- Package: `@mielui/svelte`
- Component version: `1.1.0`
- Depends on Mielui components: _internal/utils, card, button, checkbox, spinner, textarea
- Shared utilities: components/_internal/button-attributes, components/_internal/submission.svelte, hugeicons-icon, utils.cn, utils.createContext

## Install

~~~~sh
pnpm dlx @mielui/svelte add question
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Actions

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Cancel

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| onclick | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when it is clicked or activated. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Description

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Input

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| submitOnEnter | boolean \| undefined | true | No | No | Enter submits and Shift+Enter adds a line. Set false to make Enter add a line. |
| autoresize | boolean \| undefined | true | No | No | Grows with its content instead of scrolling. |
| placeholder | string \| undefined | - | No | No | Text shown while there is no value. |
| aria-label | string \| undefined | - | No | No | Accessible name of the answer field. |
| rows | number \| undefined | 2 | No | No | Starting height in text rows. |
| disabled | boolean \| undefined | false | No | No | Prevents interaction and dims the control. |
| readonly | boolean \| undefined | false | No | No | Shows the value without allowing edits. The value is still submitted. |
| element | HTMLTextAreaElement \| undefined | - | No | Yes | Bindable reference to the DOM element. |
| class | string \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Option

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| value | string | - | Yes | No | Value this option answers with. |
| label | string | - | Yes | No | Visible label of the option. |
| description | string \| undefined | - | No | No | Supporting text under the option label. |
| disabled | boolean \| undefined | false | No | No | Prevents interaction and dims the control. |
| element | HTMLInputElement \| undefined | - | No | Yes | Bindable reference to the DOM element. |
| class | string \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Options

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| labels | QuestionLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| variant | "default" \| "inset" \| undefined | 'default' | No | No | `inset` frames the question with its actions on the frame. |
| status | QuestionStatus \| undefined | 'idle' | No | No | Current state, which drives the submit control. |
| disabled | boolean \| undefined | false | No | No | Prevents interaction and dims the control. |
| required | boolean \| undefined | true | No | No | Requires an answer before submitting. |
| autofocus | boolean \| undefined | false | No | No | Focuses the first control when it mounts. |
| name | string \| undefined | 'answer' | No | No | Field name of the answer. |
| onError | ((error: unknown) => void) \| undefined | - | No | No | Called with the error when `onSubmit` rejects. |
| onCancel | ((event: MouseEvent) => void) \| undefined | - | No | No | Called when Cancel is pressed. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| type | "single" \| "text" \| "multiple" \| undefined | 'single' | No | No | `single` and `multiple` offer options. `text` takes a written answer. |
| value | string \| string[] \| undefined | - | No | Yes | Current answer. Bindable. |
| onSubmit | (answer: string, event: SubmitEvent) => void \| Promise<void> \| (answer: string[], event: SubmitEvent) => void \| Promise<void> | - | Yes | No | Called with the answer. Return a promise to show progress until it settles. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Submit

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| element | HTMLButtonElement \| HTMLAnchorElement \| undefined | - | No | Yes | Bindable reference to the DOM element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Title

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { Snippet } from 'svelte';
import type {
    HTMLAttributes,
    HTMLButtonAttributes,
    HTMLFieldsetAttributes,
    HTMLFormAttributes,
    HTMLInputAttributes,
    HTMLTextareaAttributes
} from 'svelte/elements';
import Root from './question.svelte';
import Actions from './question-actions.svelte';
import Cancel from './question-cancel.svelte';
import Content from './question-content.svelte';
import Description from './question-description.svelte';
import Input from './question-input.svelte';
import Option from './question-option.svelte';
import Options from './question-options.svelte';
import Submit from './question-submit.svelte';
import Title from './question-title.svelte';

export type QuestionType = 'single' | 'multiple' | 'text';
export type QuestionAnswer = string | string[];
export type QuestionStatus = 'idle' | 'submitting' | 'error';

export type QuestionLabels = {
    /** Validation message for a required single-choice question with no answer. */
    selectAnswer?: string;
    /** Validation message for a required multiple-choice question with no answer. */
    selectAnswers?: string;
    /** Validation message for a required text question with no answer. */
    enterAnswer?: string;
    /** Message shown when `onSubmit` rejects. */
    submitError?: string;
    /** Text of the submit button. */
    submit?: string;
    /** Text of the submit button while the answer is submitting. */
    submitting?: string;
    /** Accessible name of the answer field when it has no `aria-label`. */
    answer?: string;
    /** Placeholder of the answer field when it has no `placeholder`. */
    placeholder?: string;
};

export type QuestionProps = {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: QuestionLabels;
    /** `inset` frames the question with its actions on the frame. */
    variant?: 'default' | 'inset';
    /** Current state, which drives the submit control. */
    status?: QuestionStatus;
    /** Prevents interaction and dims the control. */
    disabled?: boolean;
    /** Requires an answer before submitting. */
    required?: boolean;
    /** Focuses the first control when it mounts. */
    autofocus?: boolean;
    /** Field name of the answer. */
    name?: string;
    /** Called with the error when `onSubmit` rejects. */
    onError?: (error: unknown) => void;
    /** Called when Cancel is pressed. */
    onCancel?: (event: MouseEvent) => void;
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & Omit<
    HTMLFormAttributes,
    'children' | 'class' | 'onsubmit' | 'action' | 'method' | 'target' | 'enctype' | 'name'
> &
    (
        | {
              /** `single` and `multiple` offer options. `text` takes a written answer. */
              type?: 'single' | 'text';
              /** Current answer. Bindable. */
              value?: string;
              /** Called with the answer. Return a promise to show progress until it settles. */
              onSubmit: (answer: string, event: SubmitEvent) => void | Promise<void>;
          }
        | {
              /** `single` and `multiple` offer options. `text` takes a written answer. */
              type: 'multiple';
              /** Current answer. Bindable. */
              value?: string[];
              /** Called with the answer. Return a promise to show progress until it settles. */
              onSubmit: (answer: string[], event: SubmitEvent) => void | Promise<void>;
          }
    );

export type QuestionContentProps = {
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & Omit<HTMLFieldsetAttributes, 'children' | 'class'>;

export type QuestionTitleProps = {
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & Omit<HTMLAttributes<HTMLLegendElement>, 'children' | 'class'>;

export type QuestionDescriptionProps = {
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & Omit<HTMLAttributes<HTMLParagraphElement>, 'children' | 'class'>;

export type QuestionOptionsProps = {
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class'>;

export type QuestionOptionProps = {
    /** Value this option answers with. */
    value: string;
    /** Visible label of the option. */
    label: string;
    /** Supporting text under the option label. */
    description?: string;
    /** Prevents interaction and dims the control. */
    disabled?: boolean;
    /** Bindable reference to the DOM element. */
    element?: HTMLInputElement;
    /** Classes added to the element. */
    class?: string;
} & Omit<
    HTMLInputAttributes,
    'type' | 'value' | 'name' | 'checked' | 'disabled' | 'children' | 'class'
>;

export type QuestionInputProps = {
    /** Enter submits and Shift+Enter adds a line. Set false to make Enter add a line. */
    submitOnEnter?: boolean;
    /** Grows with its content instead of scrolling. */
    autoresize?: boolean;
    /** Text shown while there is no value. */
    placeholder?: string;
    /** Accessible name of the answer field. */
    'aria-label'?: string;
    /** Starting height in text rows. */
    rows?: number;
    /** Prevents interaction and dims the control. */
    disabled?: boolean;
    /** Shows the value without allowing edits. The value is still submitted. */
    readonly?: boolean;
    /** Bindable reference to the DOM element. */
    element?: HTMLTextAreaElement;
    /** Classes added to the element. */
    class?: string;
} & Omit<
    HTMLTextareaAttributes,
    | 'children'
    | 'class'
    | 'value'
    | 'name'
    | 'placeholder'
    | 'aria-label'
    | 'rows'
    | 'disabled'
    | 'readonly'
>;

export type QuestionActionsProps = {
    /** Classes added to the element. */
    class?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class'>;

export type QuestionActionProps = {
    /** Content rendered inside. */
    children?: Snippet;
    /** Called when it is clicked or activated. */
    onclick?: (event: MouseEvent) => void;
} & Omit<HTMLButtonAttributes, 'children' | 'onclick' | 'type'>;

export type QuestionSubmitProps = {
    /** Content rendered inside. */
    children?: Snippet;
    /** Bindable reference to the DOM element. */
    element?: HTMLButtonElement | HTMLAnchorElement;
} & Omit<HTMLButtonAttributes, 'children' | 'type'>;

export { Actions, Cancel, Content, Description, Input, Option, Options, Root, Submit, Title };
~~~~

## Examples

### Cancel

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

    let answer = $state('');
    let outcome = $state('Waiting for an answer');
</script>

<div class="flex w-full max-w-md flex-col gap-3">
    <Question.Root
        variant="inset"
        type="text"
        bind:value={answer}
        labels={{
            submitting: 'Sending'
        }}
        onSubmit={(value) => {
            outcome = `Answer: ${value}`;
        }}
        onCancel={() => {
            outcome = 'Skipped';
        }}
    >
        <Question.Content>
            <Question.Title>What should the migration notes cover?</Question.Title>
            <Question.Description>
                Enter adds a line here. Use the button to send.
            </Question.Description>
            <Question.Input
                rows={3}
                submitOnEnter={false}
                placeholder="List the breaking changes"
            />
        </Question.Content>
        <Question.Actions>
            <Question.Cancel>Skip</Question.Cancel>
            <Question.Submit />
        </Question.Actions>
    </Question.Root>
    <p class="text-sm text-foreground-muted" role="status">{outcome}</p>
</div>
~~~~

### Composer Takeover

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Composer from '@mielui/svelte/components/composer';
    import * as Conversation from '@mielui/svelte/components/conversation';
    import * as Message from '@mielui/svelte/components/message';
    import * as Question from '@mielui/svelte/components/question';

    let asking = $state(true);
    let answer = $state('');
    let chosen = $state('');
    let draft = $state('Keep the migration reversible.');
    let sentPrompt = $state('');
    let shouldFocusQuestion = $state(false);

    function answerQuestion(value: string) {
        chosen = value;
        asking = false;
    }

    function sendPrompt(value: string) {
        sentPrompt = value;
        draft = '';
    }

    function cancelQuestion() {
        asking = false;
    }
</script>

<div
    class="flex h-[34rem] w-full max-w-3xl flex-col gap-3 rounded-[var(--radius-xl)] border border-border bg-panel p-3"
>
    <Conversation.Root class="min-h-0 flex-1">
        <Conversation.Content aria-label="Migration planning conversation">
            <Message.Root from="user">
                <Message.Content>
                    Add the account migration and keep the rollout safe. I started a follow-up in
                    the composer.
                </Message.Content>
            </Message.Root>
            <Message.Root from="assistant">
                <Message.Content>
                    I can continue after I know which environment should receive the first run.
                </Message.Content>
            </Message.Root>
            {#if chosen}
                <Message.Root from="user">
                    <Message.Content>Use {chosen}.</Message.Content>
                </Message.Root>
            {/if}
            {#if sentPrompt}
                <Message.Root from="user">
                    <Message.Content>{sentPrompt}</Message.Content>
                </Message.Root>
            {/if}
        </Conversation.Content>
        <Conversation.ScrollButton />
    </Conversation.Root>

    <div>
        {#if asking}
            <Question.Root
                variant="inset"
                bind:value={answer}
                autofocus={shouldFocusQuestion}
                onSubmit={answerQuestion}
            >
                <Question.Content>
                    <Question.Title>Where should I run the migration first?</Question.Title>
                    <Question.Description>
                        Your unsent composer draft will stay in place while you answer.
                    </Question.Description>
                    <Question.Options>
                        <Question.Option
                            value="preview"
                            label="Preview environment"
                            description="Validate against a disposable copy first."
                        />
                        <Question.Option
                            value="staging"
                            label="Staging environment"
                            description="Run against the shared pre-production data."
                        />
                    </Question.Options>
                </Question.Content>
                <Question.Actions>
                    <Question.Cancel onclick={cancelQuestion}>Skip question</Question.Cancel>
                    <Question.Submit />
                </Question.Actions>
            </Question.Root>
        {:else}
            <Composer.Root bind:value={draft} onSubmit={sendPrompt}>
                <Composer.Input aria-label="Message the agent" />
                <Composer.Toolbar variant="inset">
                    <Composer.Actions>
                        <Button
                            variant="outline"
                            onclick={() => {
                                shouldFocusQuestion = true;
                                asking = true;
                            }}
                        >
                            Ask again
                        </Button>
                    </Composer.Actions>
                    <Composer.Submit />
                </Composer.Toolbar>
            </Composer.Root>
        {/if}
    </div>
</div>
~~~~

### Free Text

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

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

<div class="flex w-full max-w-2xl flex-col gap-3">
    <Question.Root
        variant="inset"
        type="text"
        bind:value={answer}
        onSubmit={(value) => {
            submitted = value;
        }}
    >
        <Question.Content>
            <Question.Title>What constraint should I preserve?</Question.Title>
            <Question.Description>
                Add any implementation detail that is not captured by the current plan.
            </Question.Description>
            <Question.Input placeholder="For example: Keep the existing API stable" />
        </Question.Content>
        <Question.Actions>
            <Question.Submit />
        </Question.Actions>
    </Question.Root>

    <p class="min-h-5 text-sm text-foreground-muted" role="status">
        {submitted ? `Answer: ${submitted}` : 'Waiting for an answer'}
    </p>
</div>
~~~~

### Labels

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

    const labels: QuestionLabels = {
        selectAnswer: 'Choisissez une réponse.',
        submitError: "La réponse n'a pas pu être envoyée.",
        submit: 'Envoyer',
        submitting: 'Envoi…'
    };

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

<div class="w-full max-w-xl">
    <Question.Root
        variant="inset"
        {labels}
        bind:value={answer}
        onSubmit={() => {
            answer = '';
        }}
    >
        <Question.Content>
            <Question.Title>Où lancer la migration en premier ?</Question.Title>
            <Question.Options>
                <Question.Option value="preview" label="Environnement de test" />
                <Question.Option value="production" label="Production" />
            </Question.Options>
        </Question.Content>
        <Question.Actions>
            <Question.Cancel>Passer</Question.Cancel>
            <Question.Submit />
        </Question.Actions>
    </Question.Root>
</div>
~~~~

### Multiple Choice

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

    let answer = $state<string[]>([]);
    let summary = $state('');
</script>

<div class="flex w-full max-w-2xl flex-col gap-3">
    <Question.Root
        variant="inset"
        type="multiple"
        bind:value={answer}
        labels={{
            submit: 'Run selected checks'
        }}
        onSubmit={(value) => {
            summary = value.join(', ');
        }}
    >
        <Question.Content>
            <Question.Title>
                Which checks should I run before opening the pull request?
            </Question.Title>
            <Question.Description>Select every check you want included.</Question.Description>
            <Question.Options>
                <Question.Option value="tests" label="Test suite" />
                <Question.Option value="types" label="Type checking" />
                <Question.Option value="build" label="Production build" />
                <Question.Option value="browser" label="Browser smoke test" />
            </Question.Options>
        </Question.Content>
        <Question.Actions>
            <Question.Submit />
        </Question.Actions>
    </Question.Root>

    <p class="min-h-5 text-sm text-foreground-muted" role="status">
        {summary ? `Queued: ${summary}` : 'No checks queued'}
    </p>
</div>
~~~~

### Series

~~~~svelte
<script lang="ts">
    import { ArrowLeft02Icon as ArrowLeft } from '@hugeicons/core-free-icons';
    import { Button } from '@mielui/svelte/components/button';
    import * as Question from '@mielui/svelte/components/question';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';

    const questions = [
        {
            title: 'Where should we start?',
            description: 'Choose a starting point for the next round of work.',
            options: [
                {
                    value: 'interface',
                    label: 'The interface',
                    description: 'Refine the screens people use every day.'
                },
                {
                    value: 'workflow',
                    label: 'The workflow',
                    description: 'Make the path from start to finish simpler.'
                },
                {
                    value: 'foundation',
                    label: 'The foundation',
                    description: 'Improve the architecture behind the product.'
                }
            ]
        },
        {
            title: 'How much should change?',
            description: 'Set the scope so the work stays focused.',
            options: [
                {
                    value: 'polish',
                    label: 'A little polish',
                    description: 'Keep the structure and refine the details.'
                },
                {
                    value: 'focused',
                    label: 'A focused update',
                    description: 'Rework one part of the experience.'
                },
                {
                    value: 'rethink',
                    label: 'A fresh direction',
                    description: 'Explore a different approach from the ground up.'
                }
            ]
        },
        {
            title: 'What should I bring back?',
            description: 'Choose what you want to review before we continue.',
            options: [
                {
                    value: 'plan',
                    label: 'A clear plan',
                    description: 'Outline the changes and their trade-offs.'
                },
                {
                    value: 'prototype',
                    label: 'A working prototype',
                    description: 'Make the idea tangible and easy to try.'
                },
                {
                    value: 'implementation',
                    label: 'The implementation',
                    description: 'Build the change and summarize the result.'
                }
            ]
        }
    ];
    let step = $state(0);
    let answers = $state<string[]>(['', '', '']);
    const complete = $derived(step === questions.length);
    const question = $derived(questions[Math.min(step, questions.length - 1)]);

    function next() {
        if (!complete) {
            step += 1;
        }
    }

    function restart() {
        answers = ['', '', ''];
        step = 0;
    }
</script>

<div class="w-full max-w-xl">
    <Question.Root
        variant="inset"
        bind:value={answers[step]}
        required={!complete}
        labels={{
            submit: step === questions.length - 1 ? 'Finish' : 'Next'
        }}
        onSubmit={next}
    >
        <Question.Content>
            {#if complete}
                <Question.Title>Ready to get started</Question.Title>
                <Question.Description>
                    Here’s the direction you chose. You can go back to adjust it.
                </Question.Description>
                <dl class="grid gap-4 px-4 py-5">
                    {#each questions as item, index (item.title)}
                        <div class="grid gap-1">
                            <dt class="text-xs text-foreground-muted">{item.title}</dt>
                            <dd class="m-0 text-sm font-medium">
                                {item.options.find((option) => option.value === answers[index])?.label}
                            </dd>
                        </div>
                    {/each}
                </dl>
            {:else}
                <Question.Title>{question.title}</Question.Title>
                <Question.Description>{question.description}</Question.Description>
                <Question.Options>
                    {#each question.options as option (option.value)}
                        <Question.Option {...option} />
                    {/each}
                </Question.Options>
            {/if}
        </Question.Content>
        <Question.Actions>
            <span class="me-auto text-xs tabular-nums text-foreground-muted" role="status">
                {complete ? 'All questions answered' : `Question ${step + 1} of ${questions.length}`}
            </span>
            <Question.Cancel
                disabled={step === 0}
                onclick={(event) => {
                    event.preventDefault();
                    step -= 1;
                }}
            >
                <HugeiconsIcon icon={ArrowLeft} size={14} aria-hidden="true" />
                Back
            </Question.Cancel>
            {#if complete}
                <Button type="button" variant="primary" size="md" onclick={restart}>
                    Start again
                </Button>
            {:else}
                <Question.Submit />
            {/if}
        </Question.Actions>
    </Question.Root>
</div>
~~~~

For the rendered reference, visit [/docs/components/question](/docs/components/question).
