HextaUI

Questionário

Um formulário passo a passo que se move entre perguntas com senso de direção, mostra o progresso real e responde pelo teclado.

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

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

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>

As respostas são enviadas como um formulário comum, indexadas pelo name de cada item. As perguntas se movem na direção em que você vai, o card suaviza até a altura de cada pergunta, e o progresso só preenche para perguntas respondidas ou puladas.

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

Transição lift e barra

transition="lift" move as perguntas para cima e para baixo como uma pilha. variant="bar" no progresso mostra uma única barra contínua.

"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>
  )
}

Avanço automático

Com autoAdvance, escolher uma resposta única avança após uma breve pausa, então você pode responder só com 1–5. Alterar uma resposta anterior nunca salta adiante, e a última pergunta nunca é enviada sozinha.

"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>
  )
}

Perguntas condicionais

Desabilite um item para deixá-lo de fora. Ele sai do progresso e do fluxo até passar a valer.

"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>
  )
}

Da direita para a esquerda

O deslizamento segue a direção de leitura.

"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>
  )
}
TeclaAção
↑↓Move entre as respostas, incluindo o campo de texto. Mover não escolhe.
SpaceEscolhe a resposta com foco, ou a alterna em múltipla escolha.
EnterContinua. Em uma resposta única não escolhida, escolhe-a primeiro. Envia na última pergunta.
←→Pergunta anterior ou próxima, espelhado da direita para a esquerda. Next treme quando uma resposta é necessária. Dentro do campo de texto, movem o cursor.
HomeEndPrimeira ou última resposta.
A1Escolhe uma resposta pela sua tecla quando shortcuts está definido. As teclas se iluminam quando o questionnaire recebe foco; clique em qualquer lugar nele para começar.
EscSai do campo de texto para os atalhos de letras voltarem a funcionar.
⌘EnterEnvia de qualquer lugar.
  • Cada QuestionnaireItem é um <fieldset> cujo título é seu <legend>. Perguntas inativas ficam ocultas e inertes, inclusive a que está animando a saída.
  • O progresso é uma progressbar que anuncia “Question 2 of 5”; os segmentos e o número giratório são decorativos.
  • Os erros usam role="alert" e as respostas inválidas recebem aria-invalid.
  • Com movimento reduzido, as perguntas trocam instantaneamente e nada treme.
PropTipoPadrão
transition
"slide" | "lift""slide"
autoAdvanceAvança 350ms depois que uma resposta única é escolhida pela primeira vez.
booleanfalse
shortcutsAdiciona um atalho de tecla a cada resposta.
"letters" | "numbers"–
item
string–
defaultItem
string–
onItemChange
(item: string) => void–
AtributoDescrição
data-slot="questionnaire"O formulário, com data-transition.
data-current / data-totalA posição atual e o número de perguntas.
data-first / data-lastPresente na primeira ou na última pergunta.
PropTipoPadrão
variant
"segments" | "bar""segments"
AtributoDescrição
data-slot="questionnaire-progress-segment"Um por pergunta, com data-status, data-active e data-invalid.
PropTipoPadrão
nameO nome do campo do formulário.
string–
required
booleanfalse
multipleCheckboxes em vez de radios.
booleanfalse
disabledDeixa a pergunta fora do fluxo.
booleanfalse
onStatusChange
(status) => void–
AtributoDescrição
data-status"unanswered", "answered" ou "skipped".
data-activeA pergunta atual.
data-invalidPrecisa de uma resposta.
data-leavingA pergunta animando a saída.
PropTipoPadrão
value
string–
defaultChecked
boolean–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<label>
AtributoDescrição
data-checked / data-uncheckedSe a resposta está escolhida.
data-type"radio" ou "checkbox".
data-shortcutA tecla da resposta.

Uma resposta de texto livre que fica junto das opções. Digitar limpa a opção escolhida. Dê a ela um rótulo ou aria-label.

PropTipoPadrão
variant
Button variant"ghost", or "default" for Next and Submit
size
Button size"default"

QuestionnaireNext e QuestionnaireSubmit compartilham um slot, então na última pergunta Next troca por Submit sem mover nada.

Usado em blocos

Blocos que se baseiam em Questionnaire.