# Questions



import QuestionsDefault from "@/components/nexus-ui/examples/questions/default";
import QuestionsMultipleChoice from "@/components/nexus-ui/examples/questions/multiple-choice";
import QuestionsMultipleQuestions from "@/components/nexus-ui/examples/questions/multiple-questions";
import QuestionsRequired from "@/components/nexus-ui/examples/questions/required";
import QuestionsFixedOptions from "@/components/nexus-ui/examples/questions/fixed-options";
import { Callout } from "@/components/callout";

Composable card for **follow-up clarification questions** in chat—when the model (or your app) needs structured input before continuing. Pass **`items`** for synchronous question metadata (title, pagination, navigation), compose each slide with **`Question`**, **`QuestionOption`**, and optional **`QuestionOther`**, then handle **`onSubmit`** with a typed submission array.

Single-choice answers **auto-advance** to the next question (except on the last slide, where **Submit** is required). **Skip** moves past optional unanswered questions without storing an answer. Skipped or unanswered optional questions are emitted with status **`skipped`** and answer **`[No Preference]`**. Only explicit selections are kept in internal state. **Submit** is disabled only while a **required** question is unanswered—hide it until the last slide in multi-question flows.

<DemoWithCode src="components/nexus-ui/examples/questions/default.tsx">
  <QuestionsDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add carousel button card checkbox && npm install @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add carousel button card checkbox && pnpm add @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add carousel button card checkbox && yarn add @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add carousel button card checkbox && bun add @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/questions.tsx" title="components/nexus-ui/questions.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground noCollapse
import {
  Question,
  QuestionOption,
  QuestionOptions,
  QuestionOther,
  Questions,
  QuestionsFooter,
  QuestionsHeader,
  QuestionsSubmit,
  QuestionsTitle,
  type QuestionInput,
} from "@/components/nexus-ui/questions";
```

```tsx keepBackground noCollapse
const question: QuestionInput = {
  id: "depth",
  type: "single",
  prompt: "How detailed should the explanation be?",
  options: [
    { value: "brief", label: "Brief overview" },
    { value: "standard", label: "Standard depth" },
  ],
};

<Questions
  items={[question]}
  onSubmit={(submission) => console.log(submission)}
>
  <QuestionsHeader>
    <QuestionsTitle />
  </QuestionsHeader>

  <Question id={question.id}>
    <QuestionOptions>
      {question.options.map((option) => (
        <QuestionOption key={option.value} value={option.value}>
          {option.label}
        </QuestionOption>
      ))}
      <QuestionOther />
    </QuestionOptions>
  </Question>

  <QuestionsFooter>
    <QuestionsSubmit />
  </QuestionsFooter>
</Questions>
```

Pass **`items`** so **`QuestionsTitle`** and carousel pagination have question metadata on the first paint. For a single question, compose **`Question`** directly without **`QuestionsCarousel`**; use the carousel primitives when there are multiple slides.

Examples [#examples]

Multiple choice [#multiple-choice]

Set **`type: "multiple"`** in **`items`** to render checkbox rows. Users select one or more options before submitting—multiple choice does not auto-advance.

<DemoWithCode src="components/nexus-ui/examples/questions/multiple-choice.tsx">
  <QuestionsMultipleChoice />
</DemoWithCode>

Multiple questions [#multiple-questions]

For two or more prompts, wrap slides in **`QuestionsCarousel`**. Prev/next controls and pagination stay in sync with the active index; required questions block forward navigation until answered. Use **`disableUntilLastQuestion`** on **`QuestionsSubmit`** to keep submit visible but disabled until the final slide (and required questions are answered).

<DemoWithCode src="components/nexus-ui/examples/questions/multiple-questions.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <QuestionsMultipleQuestions />
</DemoWithCode>

Required questions [#required-questions]

Set **`required: true`** on every question in **`items`** to block forward navigation until each slide is answered. Omit **`QuestionsSkip`** when nothing is optional—skip only applies to unanswered optional questions. Omit **`QuestionsDismiss`** (and **`onDismiss`**) when you want a forced flow.

<DemoWithCode src="components/nexus-ui/examples/questions/required.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <QuestionsRequired />
</DemoWithCode>

Fixed options [#fixed-options]

Omit **`QuestionOther`** when the question should only accept predefined options.

<DemoWithCode src="components/nexus-ui/examples/questions/fixed-options.tsx">
  <QuestionsFixedOptions />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Use **Questions** when an agent needs structured clarification before continuing—for example a **`askClarifyingQuestions`** tool that streams arguments to the client and waits for the user's answers.

The usual pattern:

1. Define a tool **without** **`execute`** so the model stops after emitting question definitions.
2. Render **`Questions`** when the tool part reaches **`input-available`**.
3. Call **`addToolOutput`** (or **`addToolResult`**) in **`onSubmit`** with the submission payload.
4. Optionally use **`sendAutomaticallyWhen`** so the chat resumes after the tool output is added.

<Callout type="info">
  This flow is client-side tool handling. The model proposes questions; your UI collects answers and returns them as the tool result so the agent can continue.
</Callout>

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react zod
    ```
  </Step>

  <Step>
    <h3>
      Expose a clarification tool on your chat route
    </h3>

    Define a tool with a Zod schema that matches **`QuestionInput`**. Omit **`execute`** so the client must supply the result.

    ```ts title="app/api/chat/route.ts"
    import { convertToModelMessages, streamText, tool, type UIMessage } from "ai";
    import { z } from "zod";

    const questionSchema = z.object({
      id: z.string(),
      type: z.enum(["single", "multiple"]),
      prompt: z.string(),
      required: z.boolean().optional(),
      options: z.array(
        z.object({
          value: z.string(),
          label: z.string(),
        }),
      ),
    });

    export async function POST(req: Request) {
      const { messages }: { messages: UIMessage[] } = await req.json();

      const result = streamText({
        model: "anthropic/claude-sonnet-4.5",
        system:
          "When the user's request is ambiguous, call askClarifyingQuestions before answering.",
        messages: await convertToModelMessages(messages),
        tools: {
          askClarifyingQuestions: tool({
            description:
              "Ask the user one or more clarifying questions before continuing.",
            inputSchema: z.object({
              questions: z.array(questionSchema).min(1),
            }),
          }),
        },
      });

      return result.toUIMessageStreamResponse();
    }
    ```
  </Step>

  <Step>
    <h3>
      Render tool input as Questions and return answers with 

      <code className="tracking-0!">addToolOutput</code>
    </h3>

    Find the latest assistant message with an **`askClarifyingQuestions`** tool part in **`input-available`** state, map **`part.input.questions`** to **`items`**, and wire **`onSubmit`** to **`addToolOutput`**. Use **`sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls`** (or your own predicate) so the model continues after answers are submitted.

    ```tsx title="app/clarification-chat/page.tsx"
    "use client";

    import { useChat } from "@ai-sdk/react";
    import {
      DefaultChatTransport,
      lastAssistantMessageIsCompleteWithToolCalls,
      type UIMessage,
    } from "ai";
    import {
      Question,
      QuestionOption,
      QuestionOptions,
      QuestionOther,
      Questions,
      QuestionsCarousel,
      QuestionsCarouselContent,
      QuestionsCarouselIndex,
      QuestionsCarouselItem,
      QuestionsCarouselNext,
      QuestionsCarouselPagination,
      QuestionsCarouselPrev,
      QuestionsFooter,
      QuestionsHeader,
      QuestionsSkip,
      QuestionsSubmit,
      QuestionsTitle,
      type QuestionInput,
      type QuestionsSubmission,
    } from "@/components/nexus-ui/questions";

    type ClarifyingQuestionsInput = {
      questions: QuestionInput[];
    };

    type ClarifyingQuestionsToolPart = {
      type: "tool-askClarifyingQuestions";
      toolCallId: string;
      state: "input-available" | "output-available" | "output-error";
      input?: ClarifyingQuestionsInput;
    };

    function findPendingClarification(messages: UIMessage[]) {
      const assistant = [...messages].reverse().find((m) => m.role === "assistant");
      if (!assistant) return null;

      return assistant.parts.find(
        (part): part is ClarifyingQuestionsToolPart =>
          part.type === "tool-askClarifyingQuestions" &&
          part.state === "input-available" &&
          Boolean((part as ClarifyingQuestionsToolPart).input?.questions?.length),
      );
    }

    export default function ClarificationChatPage() {
      const { messages, addToolOutput, status } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
        sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
      });

      const pending = findPendingClarification(messages);
      if (!pending?.input?.questions) return null;

      const items = pending.input.questions;

      function handleSubmit(submission: QuestionsSubmission) {
        void addToolOutput({
          tool: "askClarifyingQuestions",
          toolCallId: pending.toolCallId,
          output: { answers: submission },
        });
      }

      return (
        <Questions items={items} onSubmit={handleSubmit}>
          <QuestionsCarousel>
            <QuestionsHeader>
              <QuestionsTitle />
              {items.length > 1 ? (
                <QuestionsCarouselPagination>
                  <QuestionsCarouselPrev />
                  <QuestionsCarouselIndex format="of" />
                  <QuestionsCarouselNext />
                </QuestionsCarouselPagination>
              ) : null}
            </QuestionsHeader>

            <QuestionsCarouselContent className="mx-0">
              {items.map((question) => (
                <QuestionsCarouselItem key={question.id}>
                  <Question id={question.id}>
                    <QuestionOptions>
                      {question.options.map((option) => (
                        <QuestionOption key={option.value} value={option.value}>
                          {option.label}
                        </QuestionOption>
                      ))}
                      <QuestionOther />
                    </QuestionOptions>
                  </Question>
                </QuestionsCarouselItem>
              ))}
            </QuestionsCarouselContent>
          </QuestionsCarousel>

          <QuestionsFooter>
            {items.length > 1 ? <QuestionsSkip /> : null}
            <QuestionsSubmit showOnLastQuestion disabled={status !== "ready"} />
          </QuestionsFooter>
        </Questions>
      );
    }
    ```
  </Step>

  <Step>
    <h3>
      Alternative: send answers as a user message
    </h3>

    If you are not using tools, format **`QuestionsSubmission`** and call **`sendMessage`** directly—useful for lightweight clarification UIs that do not participate in the tool loop.

    ```tsx
    import type { QuestionsSubmission } from "@/components/nexus-ui/questions";

    function formatAnswers(submission: QuestionsSubmission) {
      return submission
        .map((entry) => {
          if (entry.type === "single") {
            return `- ${entry.prompt}: ${entry.answer.label}`;
          }
          const labels = entry.answer.map((item) => item.label).join(", ");
          return `- ${entry.prompt}: ${labels}`;
        })
        .join("\n");
    }

    // onSubmit={(submission) => sendMessage({ text: formatAnswers(submission) })}
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Questions [#questions]

Root card and state container. Manages answer state, active index, carousel sync, skip/submit rules, and typed submission payloads. Extends shadcn **[Card](https://ui.shadcn.com/docs/components/radix/card)**.

<TypeTable
  type={{
  items: {
    type: "QuestionInput[]",
    description:
      "Required question metadata and options. Drives title, pagination, navigation, and submission labels. Map the same data into Question slides for options UI.",
  },
  autoAdvance: {
    type: "boolean",
    default: "true",
    description:
      "When true, single-choice option clicks advance to the next slide. Other input never auto-advances. No-op on the last question.",
  },
  onSubmit: {
    type: "(submission: QuestionsSubmission) => void",
    description:
      "Fired when Submit is clicked and every required question is answered. Skipped or unanswered optional questions use answer label \"[No Preference]\". Clears selections and resets to the first question.",
  },
  onSkip: {
    type: "(questionId: string) => void",
    description: "Called when Skip advances past the current question.",
  },
  onDismiss: {
    type: "() => void",
    description: "Called when QuestionsDismiss is clicked.",
  },
  className: {
    type: "string",
    description: "Merged with the default card shell styles.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Composition tree: header, carousel or question slides, and footer.",
  },
}}
/>

QuestionsHeader [#questionsheader]

Top bar layout shell for title, pagination, and dismiss controls. Extends shadcn **`CardHeader`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default header row layout.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Typically QuestionsTitle, QuestionsCarouselPagination, and QuestionsDismiss.",
  },
}}
/>

QuestionsTitle [#questionstitle]

Renders the active question prompt from **`items[root.index]`** in **`CardTitle`**, unless **`children`** override it. Extends shadcn **`CardTitle`**.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description:
      "Optional override. Defaults to the prompt from items at the active index.",
  },
  className: {
    type: "string",
    description: "Merged with default title styles.",
  },
}}
/>

QuestionsDismiss [#questionsdismiss]

Icon dismiss control. Invokes **`onDismiss`** on the root. Extends shadcn **`Button`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default dismiss button styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called before onDismiss runs.",
  },
}}
/>

QuestionsCarousel [#questionscarousel]

[**Carousel**](https://ui.shadcn.com/docs/components/radix/carousel) root for multi-question slides. Registers the Embla API on **`Questions`** for index sync and animated viewport height.

<TypeTable
  type={{
  setApi: {
    type: "(api: CarouselApi | undefined) => void",
    description:
      "Optional; invoked when the Embla instance is ready or cleared.",
  },
  className: {
    type: "string",
    description: "Classes on the carousel region wrapper.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically QuestionsHeader and QuestionsCarouselContent.",
  },
}}
/>

QuestionsCarouselPagination [#questionscarouselpagination]

Flex row for prev / index / next controls.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default pagination row layout.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Typically QuestionsCarouselPrev, QuestionsCarouselIndex, and QuestionsCarouselNext.",
  },
}}
/>

QuestionsCarouselPrev [#questionscarouselprev]

Previous-slide button. Disabled on the first question.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description: "Replace the default arrow icon.",
  },
  className: {
    type: "string",
    description: "Merged with default circular nav button styles.",
  },
}}
/>

QuestionsCarouselIndex [#questionscarouselindex]

Active slide indicator (**1-based** current). Renders a **`span`**.

<TypeTable
  type={{
  format: {
    type: '"of" | "slash"',
    default: '"of"',
    description: 'Display as "1 of 3" or "1/3".',
  },
  className: {
    type: "string",
    description: "Merged with default tabular-nums text styles.",
  },
}}
/>

QuestionsCarouselNext [#questionscarouselnext]

Next-slide button. Disabled on the last question or when the current required question is unanswered.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description: "Replace the default arrow icon.",
  },
  className: {
    type: "string",
    description: "Merged with default circular nav button styles.",
  },
}}
/>

QuestionsCarouselContent [#questionscarouselcontent]

Carousel track wrapper. Animates viewport height to the active slide. Extends shadcn **`CarouselContent`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Classes on the flex track inside the overflow viewport.",
  },
  children: {
    type: "React.ReactNode",
    description: "QuestionsCarouselItem slides.",
  },
}}
/>

QuestionsCarouselItem [#questionscarouselitem]

One carousel slide. Wrap **`Question`** inside.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default slide layout.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically a Question with QuestionOptions.",
  },
}}
/>

Question [#question]

One question scope for option components. Wraps children in **`CardContent`**. **`id`** must match an entry in **`Questions`** **`items`**.

<TypeTable
  type={{
  id: {
    type: "string",
    description: "Stable question id; keys the answer in context.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically QuestionOptions with QuestionOption and optional QuestionOther.",
  },
}}
/>

QuestionOptions [#questionoptions]

Vertical list wrapper for **`QuestionOption`** and **`QuestionOther`**. Injects **`optionIndex`** into **`QuestionOption`** children for numbered single-choice badges.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with the default options list layout.",
  },
  children: {
    type: "React.ReactNode",
    description: "QuestionOption and QuestionOther rows.",
  },
}}
/>

QuestionOption [#questionoption]

Selectable row. Single choice renders a numbered button; clicks advance to the next slide when **`Questions`** **`autoAdvance`** is enabled (default). Multiple choice renders a checkbox label.

<TypeTable
  type={{
  value: {
    type: "string",
    description: "Answer value stored in submission.",
  },
  optionIndex: {
    type: "number",
    description: "Optional; set automatically by QuestionOptions.",
  },
  children: {
    type: "React.ReactNode",
    description: "Option label shown in the row.",
  },
  className: {
    type: "string",
    description: "Merged with default row styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description:
      "Optional click handler for single choice. Called before selection.",
  },
}}
/>

QuestionOther [#questionother]

Free-text **Other** row. Compose only when free-text answers are allowed. Uses **`QUESTION_OTHER_VALUE`** (`"__other__"`) as the stored value when selected. Typing selects **Other** (single and multiple); multiple choice also pairs with a checkbox.

<TypeTable
  type={{
  placeholder: {
    type: "string",
    default: '"Other..."',
    description: "Input placeholder text.",
  },
  className: {
    type: "string",
    description: "Merged with default row styles.",
  },
  onKeyDown: {
    type: "React.KeyboardEventHandler<HTMLInputElement>",
    description: "Forwarded to the text input.",
  },
}}
/>

QuestionsFooter [#questionsfooter]

Bottom bar layout shell for skip and submit actions. Extends shadcn **`CardFooter`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default footer layout.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically QuestionsSkip and QuestionsSubmit.",
  },
}}
/>

QuestionsSkip [#questionsskip]

Ghost **Skip** button with a fixed **Skip** label. Disabled on the last question, when the current question is required and unanswered, or when there is no current question. Extends shadcn **`Button`**.

<TypeTable
  type={{
  disabled: {
    type: "boolean",
    description:
      "Optional override. Defaults to disabled when skip is not allowed.",
  },
  className: {
    type: "string",
    description: "Merged with default skip button styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called before skip runs.",
  },
}}
/>

QuestionsSubmit [#questionssubmit]

Primary **Submit** button. Disabled while any **required** question is unanswered; optional unanswered questions are allowed. Extends shadcn **`Button`**.

<TypeTable
  type={{
  showOnLastQuestion: {
    type: "boolean",
    default: "false",
    description:
      "When true, renders nothing until the active slide is the last question.",
  },
  disableUntilLastQuestion: {
    type: "boolean",
    default: "false",
    description:
      "When true, keeps submit visible but disabled until the active slide is the last question. Still respects required-question validation via canSubmit.",
  },
  disabled: {
    type: "boolean",
    description:
      "Optional override. Defaults to disabled while a required question is unanswered.",
  },
  className: {
    type: "string",
    description: "Merged with default submit button styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called before submit runs.",
  },
  children: {
    type: "React.ReactNode",
    default: '"Submit"',
    description: "Button label.",
  },
}}
/>

Types [#types]

```ts noCollapse
export type QuestionOptionInput = {
  value: string;
  label: React.ReactNode;
};

export type QuestionInput = {
  id: string;
  type: "single" | "multiple";
  prompt: React.ReactNode;
  options: QuestionOptionInput[];
  required?: boolean;
};

export type QuestionSubmissionAnswer = {
  value: string;
  label: React.ReactNode;
};

export type QuestionsSubmission = Array<
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "single";
      status: "answered";
      answer: QuestionSubmissionAnswer;
    }
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "single";
      status: "skipped";
      answer: QuestionSubmissionAnswer;
    }
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "multiple";
      status: "answered";
      answer: QuestionSubmissionAnswer[];
    }
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "multiple";
      status: "skipped";
      answer: QuestionSubmissionAnswer[];
    }
>;

export const QUESTION_OTHER_VALUE = "__other__";
export const QUESTION_NO_PREFERENCE_VALUE = "__no_preference__";
export const QUESTION_NO_PREFERENCE_LABEL = "[No Preference]";
```
