HextaUI

Questionnaire

Un formulaire pas à pas qui passe d'une question à l'autre avec un sens de direction, montre une vraie progression et répond au clavier.

Question 1 of 3
What best describes your work?

We'll tailor the examples you see first.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoiceDescription,
  QuestionnaireChoices,
  QuestionnaireDescription,
  QuestionnaireError,
  QuestionnaireInput,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSkip,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire"

const items = [
  {
    name: "role",
    required: true,
    prompt: "What best describes your work?",
    description: "We'll tailor the examples you see first.",
    choices: [
      {
        value: "design",
        label: "Design",
        description: "Interfaces, systems and prototypes.",
      },
      {
        value: "engineering",
        label: "Engineering",
        description: "Shipping and maintaining product code.",
      },
      {
        value: "both",
        label: "A bit of both",
        description: "Design engineering, the best kind.",
      },
    ],
  },
  {
    name: "interests",
    multiple: true,
    required: true,
    prompt: "Which components do you reach for most?",
    description: "Pick as many as you like.",
    choices: [
      { value: "forms", label: "Forms and inputs" },
      { value: "overlays", label: "Dialogs and sheets" },
      { value: "navigation", label: "Menus and navigation" },
      { value: "data", label: "Tables and data" },
    ],
  },
  {
    name: "source",
    prompt: "How did you find HextaUI?",
    description: "Optional. Skip it if you don't remember.",
    choices: [
      { value: "x", label: "X / Twitter" },
      { value: "github", label: "GitHub" },
      { value: "friend", label: "A friend" },
    ],
    input: { label: "Somewhere else", placeholder: "Somewhere else…" },
  },
] as const

export function QuestionnaireDemo() {
  const [answers, setAnswers] = React.useState<[string, string][] | null>(null)

  if (answers) {
    return (
      <div className="flex w-full max-w-md flex-col items-start gap-4 rounded-xl border p-5">
        <div className="flex flex-col gap-1">
          <p className="font-medium">Thanks, that helps a lot.</p>
          <p className="text-sm text-muted-foreground">
            Here&apos;s what the form sent:
          </p>
        </div>
        <ul className="flex w-full flex-col gap-1 font-mono text-xs">
          {answers.map(([name, value], index) => (
            <li key={index} className="flex justify-between gap-4">
              <span className="text-muted-foreground">{name}</span>
              <span className="truncate">{value}</span>
            </li>
          ))}
        </ul>
        <Button variant="outline" size="sm" onClick={() => setAnswers(null)}>
          Start over
        </Button>
      </div>
    )
  }

  return (
    <div className="w-full max-w-md rounded-xl border p-5">
      <Questionnaire
        items={items}
        shortcuts="letters"
        onSubmit={(event) => {
          event.preventDefault()
          const data = new FormData(event.currentTarget)
          setAnswers(
            Array.from(data.entries(), ([name, value]) => [name, String(value)])
          )
        }}
      >
        <QuestionnaireProgress />
        {items.map((item) => (
          <QuestionnaireItem
            key={item.name}
            name={item.name}
            required={"required" in item && item.required}
            multiple={"multiple" in item && item.multiple}
          >
            <QuestionnaireTitle>{item.prompt}</QuestionnaireTitle>
            <QuestionnaireDescription>
              {item.description}
            </QuestionnaireDescription>
            <QuestionnaireChoices>
              {item.choices.map((choice) => (
                <QuestionnaireChoice key={choice.value} value={choice.value}>
                  <span className="font-medium">{choice.label}</span>
                  {"description" in choice ? (
                    <QuestionnaireChoiceDescription>
                      {choice.description}
                    </QuestionnaireChoiceDescription>
                  ) : null}
                </QuestionnaireChoice>
              ))}
              {"input" in item ? (
                <QuestionnaireInput
                  aria-label={item.input.label}
                  placeholder={item.input.placeholder}
                />
              ) : null}
            </QuestionnaireChoices>
            <QuestionnaireError />
          </QuestionnaireItem>
        ))}
        <QuestionnaireActions>
          <QuestionnairePrevious />
          <QuestionnaireSkip />
          <QuestionnaireNext />
          <QuestionnaireSubmit />
        </QuestionnaireActions>
      </Questionnaire>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/questionnaire.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoices,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire"
<Questionnaire onSubmit={handleSubmit}>
  <QuestionnaireProgress />
  <QuestionnaireItem name="role" required>
    <QuestionnaireTitle>What best describes your work?</QuestionnaireTitle>
    <QuestionnaireChoices>
      <QuestionnaireChoice value="design">Design</QuestionnaireChoice>
      <QuestionnaireChoice value="engineering">Engineering</QuestionnaireChoice>
    </QuestionnaireChoices>
  </QuestionnaireItem>
  <QuestionnaireActions>
    <QuestionnairePrevious />
    <QuestionnaireNext />
    <QuestionnaireSubmit />
  </QuestionnaireActions>
</Questionnaire>

Les réponses sont envoyées comme un formulaire classique, indexées par le name de chaque élément. Les questions se déplacent dans le sens où vous allez, la carte s'adapte en douceur à la hauteur de chaque question, et la progression ne se remplit que pour les questions répondues ou passées.

Questionnaire
├── QuestionnaireProgress
├── QuestionnaireItem
│   ├── QuestionnaireTitle
│   ├── QuestionnaireDescription
│   ├── QuestionnaireChoices
│   │   ├── QuestionnaireChoice
│   │   │   └── QuestionnaireChoiceDescription
│   │   └── QuestionnaireInput
│   └── QuestionnaireError
└── QuestionnaireActions
    ├── QuestionnairePrevious
    ├── QuestionnaireSkip
    ├── QuestionnaireNext
    └── QuestionnaireSubmit

Transition lift et barre

transition="lift" déplace les questions vers le haut et le bas comme une pile. variant="bar" sur la progression affiche une seule barre continue.

"use client"

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoices,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire"

const items = [
  {
    name: "team",
    prompt: "How big is your team?",
    choices: ["Just me", "2–10", "11–50", "More than 50"],
  },
  {
    name: "stage",
    prompt: "Where is your product today?",
    choices: ["Idea", "In development", "Launched"],
  },
  {
    name: "timeline",
    prompt: "When do you want to ship?",
    choices: ["This month", "This quarter", "No rush"],
  },
]

export function QuestionnaireLift() {
  return (
    <div className="w-full max-w-md rounded-xl border p-5">
      <Questionnaire
        transition="lift"
        onSubmit={(event) => event.preventDefault()}
      >
        <QuestionnaireProgress variant="bar" />
        {items.map((item) => (
          <QuestionnaireItem key={item.name} name={item.name} required>
            <QuestionnaireTitle>{item.prompt}</QuestionnaireTitle>
            <QuestionnaireChoices>
              {item.choices.map((choice) => (
                <QuestionnaireChoice key={choice} value={choice}>
                  {choice}
                </QuestionnaireChoice>
              ))}
            </QuestionnaireChoices>
          </QuestionnaireItem>
        ))}
        <QuestionnaireActions>
          <QuestionnairePrevious />
          <QuestionnaireNext />
          <QuestionnaireSubmit />
        </QuestionnaireActions>
      </Questionnaire>
    </div>
  )
}

Avance automatique

Avec autoAdvance, choisir une réponse unique passe à la suite après une courte pause, ce qui permet de répondre avec 1–5 seulement. Modifier une réponse précédente ne fait jamais avancer, et la dernière question ne s'envoie jamais toute seule.

"use client"

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoices,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire"

const scale = ["Not at all", "Slightly", "Moderately", "Very", "Extremely"]

const items = [
  { name: "useful", prompt: "How useful was today's session?" },
  { name: "pace", prompt: "How comfortable was the pace?" },
  { name: "recommend", prompt: "How likely are you to come back?" },
]

export function QuestionnaireAutoAdvance() {
  return (
    <div className="w-full max-w-md rounded-xl border p-5">
      <Questionnaire
        autoAdvance
        shortcuts="numbers"
        onSubmit={(event) => event.preventDefault()}
      >
        <QuestionnaireProgress />
        {items.map((item) => (
          <QuestionnaireItem key={item.name} name={item.name} required>
            <QuestionnaireTitle>{item.prompt}</QuestionnaireTitle>
            <QuestionnaireChoices>
              {scale.map((label) => (
                <QuestionnaireChoice key={label} value={label}>
                  {label}
                </QuestionnaireChoice>
              ))}
            </QuestionnaireChoices>
          </QuestionnaireItem>
        ))}
        <QuestionnaireActions>
          <QuestionnairePrevious />
          <QuestionnaireNext />
          <QuestionnaireSubmit />
        </QuestionnaireActions>
      </Questionnaire>
    </div>
  )
}

Questions conditionnelles

Désactivez un élément pour l'écarter. Il disparaît de la progression et du parcours tant qu'il ne s'applique pas.

"use client"

import * as React from "react"

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoices,
  QuestionnaireDescription,
  QuestionnaireInput,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire"

export function QuestionnaireConditional() {
  const [usesFramework, setUsesFramework] = React.useState<string | null>(null)

  return (
    <div className="w-full max-w-md rounded-xl border p-5">
      <Questionnaire onSubmit={(event) => event.preventDefault()}>
        <QuestionnaireProgress />
        <QuestionnaireItem name="framework" required>
          <QuestionnaireTitle>Do you use a React framework?</QuestionnaireTitle>
          <QuestionnaireChoices>
            {["Yes", "No"].map((value) => (
              <QuestionnaireChoice
                key={value}
                value={value}
                onChange={(event) => setUsesFramework(event.target.value)}
              >
                {value}
              </QuestionnaireChoice>
            ))}
          </QuestionnaireChoices>
        </QuestionnaireItem>
        <QuestionnaireItem
          name="which"
          required
          disabled={usesFramework !== "Yes"}
        >
          <QuestionnaireTitle>Which one?</QuestionnaireTitle>
          <QuestionnaireDescription>
            Only asked when you use one. The progress counts it only then.
          </QuestionnaireDescription>
          <QuestionnaireChoices>
            {["Next.js", "React Router", "TanStack Start"].map((value) => (
              <QuestionnaireChoice key={value} value={value}>
                {value}
              </QuestionnaireChoice>
            ))}
          </QuestionnaireChoices>
        </QuestionnaireItem>
        <QuestionnaireItem name="email">
          <QuestionnaireTitle>
            Where can we send the results?
          </QuestionnaireTitle>
          <QuestionnaireChoices>
            <QuestionnaireInput
              type="email"
              aria-label="Email"
              placeholder="[email protected]"
            />
          </QuestionnaireChoices>
        </QuestionnaireItem>
        <QuestionnaireActions>
          <QuestionnairePrevious />
          <QuestionnaireNext />
          <QuestionnaireSubmit />
        </QuestionnaireActions>
      </Questionnaire>
    </div>
  )
}

De droite à gauche

Le glissement suit le sens de lecture.

"use client"

import {
  Questionnaire,
  QuestionnaireActions,
  QuestionnaireChoice,
  QuestionnaireChoices,
  QuestionnaireItem,
  QuestionnaireNext,
  QuestionnairePrevious,
  QuestionnaireProgress,
  QuestionnaireSubmit,
  QuestionnaireTitle,
} from "@/components/ui/questionnaire"

const items = [
  {
    name: "language",
    prompt: "ما اللغة التي تفضلها؟",
    choices: ["العربية", "الإنجليزية", "كلاهما"],
  },
  {
    name: "theme",
    prompt: "أي مظهر تستخدم؟",
    choices: ["فاتح", "داكن", "حسب النظام"],
  },
]

export function QuestionnaireRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md rounded-xl border p-5">
      <Questionnaire onSubmit={(event) => event.preventDefault()}>
        <QuestionnaireProgress />
        {items.map((item) => (
          <QuestionnaireItem key={item.name} name={item.name} required>
            <QuestionnaireTitle>{item.prompt}</QuestionnaireTitle>
            <QuestionnaireChoices>
              {item.choices.map((choice) => (
                <QuestionnaireChoice key={choice} value={choice}>
                  {choice}
                </QuestionnaireChoice>
              ))}
            </QuestionnaireChoices>
          </QuestionnaireItem>
        ))}
        <QuestionnaireActions>
          <QuestionnairePrevious>السابق</QuestionnairePrevious>
          <QuestionnaireNext>التالي</QuestionnaireNext>
          <QuestionnaireSubmit>إرسال</QuestionnaireSubmit>
        </QuestionnaireActions>
      </Questionnaire>
    </div>
  )
}
ToucheAction
↑↓Se déplace entre les réponses, y compris le champ de texte. Se déplacer ne sélectionne pas.
SpaceChoisit la réponse qui a le focus, ou la bascule en choix multiple.
EnterContinue. Sur une réponse unique non choisie, la sélectionne d'abord. Envoie à la dernière question.
←→Question précédente ou suivante, inversées en droite à gauche. Suivant tremble quand une réponse est requise. Dans le champ de texte, elles déplacent le curseur.
HomeEndPremière ou dernière réponse.
A1Choisit une réponse par sa touche quand shortcuts est défini. Les capuchons s'éclaircissent une fois que le questionnaire a le focus ; cliquez n'importe où dedans pour commencer.
EscQuitte le champ de texte pour que les raccourcis à lettre refonctionnent.
⌘EnterEnvoie depuis n'importe où.
  • Chaque QuestionnaireItem est un <fieldset> dont le titre est son <legend>. Les questions inactives sont masquées et inertes, y compris celle qui sort en animation.
  • La progression est une progressbar qui annonce « Question 2 sur 5 » ; les segments et le nombre qui tourne sont décoratifs.
  • Les erreurs utilisent role="alert" et les réponses invalides reçoivent aria-invalid.
  • Avec la réduction des animations, les questions changent instantanément et rien ne tremble.
PropTypePar défaut
transition
"slide" | "lift""slide"
autoAdvancePasse à la suite 350ms après le premier choix d'une réponse unique.
booleanfalse
shortcutsAjoute un raccourci clavier à chaque réponse.
"letters" | "numbers"–
item
string–
defaultItem
string–
onItemChange
(item: string) => void–
AttributDescription
data-slot="questionnaire"Le formulaire, avec data-transition.
data-current / data-totalLa position actuelle et le nombre de questions.
data-first / data-lastPrésent sur la première ou la dernière question.
PropTypePar défaut
variant
"segments" | "bar""segments"
AttributDescription
data-slot="questionnaire-progress-segment"Un par question, avec data-status, data-active et data-invalid.
PropTypePar défaut
nameLe nom du champ de formulaire.
string–
required
booleanfalse
multipleDes cases à cocher au lieu de boutons radio.
booleanfalse
disabledÉcarte la question du parcours.
booleanfalse
onStatusChange
(status) => void–
AttributDescription
data-status"unanswered", "answered" ou "skipped".
data-activeLa question courante.
data-invalidDemande une réponse.
data-leavingLa question qui sort en animation.
PropTypePar défaut
value
string–
defaultChecked
boolean–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<label>
AttributDescription
data-checked / data-uncheckedIndique si la réponse est choisie.
data-type"radio" ou "checkbox".
data-shortcutLa touche de la réponse.

Une réponse en texte libre qui se place avec les choix. Saisir du texte efface le choix sélectionné. Donnez-lui un label ou un aria-label.

PropTypePar défaut
variant
Button variant"ghost", or "default" for Next and Submit
size
Button size"default"

QuestionnaireNext et QuestionnaireSubmit partagent un emplacement : à la dernière question, Suivant est remplacé par Envoyer sans que rien ne bouge.

Utilisé dans les blocks

Des blocks qui s’appuient sur Questionnaire.