HextaUI

Cuestionario

Un formulario paso a paso que se mueve entre preguntas con sensación de dirección, muestra el progreso real y responde desde el 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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que 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>

Las respuestas se envían como un formulario normal, identificadas por el name de cada elemento. Las preguntas se mueven hacia la dirección en la que vas, la tarjeta se ajusta suavemente a la altura de cada pregunta, y el progreso solo se rellena con las preguntas respondidas u omitidas.

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

Transición lift y barra

transition="lift" mueve las preguntas hacia arriba y abajo como una pila. variant="bar" en el progreso muestra una barra continua.

"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 automático

Con autoAdvance, elegir una respuesta única avanza tras una breve pausa, así que puedes responder solo con 1–5. Cambiar una respuesta anterior nunca salta hacia delante, y la última pregunta nunca se envía sola.

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

Preguntas condicionales

Deshabilita un elemento para dejarlo fuera. Desaparece del progreso y del flujo hasta que corresponda.

"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 derecha a izquierda

El deslizamiento sigue la dirección de lectura.

"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>
  )
}
KeyAcción
↑↓Se mueve entre las respuestas, incluido el campo de texto. Moverse no elige.
SpaceElige la respuesta enfocada, o la alterna en opción múltiple.
EnterContinúa. Si una respuesta única no está elegida, la elige primero. En la última pregunta, envía.
←→Pregunta anterior o siguiente, invertidas en derecha a izquierda. Next vibra cuando falta una respuesta. Dentro del campo de texto mueven el cursor.
HomeEndPrimera o última respuesta.
A1Elige una respuesta por su tecla cuando se define shortcuts. Las teclas se iluminan cuando el cuestionario tiene el foco; haz clic en cualquier punto para empezar.
EscSale del campo de texto para que los atajos de letras vuelvan a funcionar.
⌘EnterEnvía desde cualquier punto.
  • Cada QuestionnaireItem es un <fieldset> cuyo título es su <legend>. Las preguntas inactivas están ocultas e inertes, incluida la que se anima al salir.
  • El progreso es un progressbar que anuncia “Pregunta 2 de 5”; los segmentos y el número giratorio son decorativos.
  • Los errores usan role="alert" y las respuestas no válidas reciben aria-invalid.
  • Con movimiento reducido, las preguntas cambian al instante y nada vibra.
PropTipoPredeterminado
transition
"slide" | "lift""slide"
autoAdvanceAvanza 350ms después de que se elija por primera vez una respuesta única.
booleanfalse
shortcutsAñade un atajo de teclado a cada respuesta.
"letters" | "numbers"–
item
string–
defaultItem
string–
onItemChange
(item: string) => void–
AtributoDescripción
data-slot="questionnaire"El formulario, con data-transition.
data-current / data-totalLa posición actual y el número de preguntas.
data-first / data-lastPresente en la primera o la última pregunta.
PropTipoPredeterminado
variant
"segments" | "bar""segments"
AtributoDescripción
data-slot="questionnaire-progress-segment"Uno por pregunta, con data-status, data-active y data-invalid.
PropTipoPredeterminado
nameEl nombre del campo del formulario.
string–
required
booleanfalse
multipleCheckboxes en lugar de radios.
booleanfalse
disabledDeja la pregunta fuera del flujo.
booleanfalse
onStatusChange
(status) => void–
AtributoDescripción
data-status"unanswered", "answered" o "skipped".
data-activeLa pregunta actual.
data-invalidNecesita una respuesta.
data-leavingLa pregunta que se anima al salir.
PropTipoPredeterminado
value
string–
defaultChecked
boolean–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<label>
AtributoDescripción
data-checked / data-uncheckedSi la respuesta está elegida.
data-type"radio" o "checkbox".
data-shortcutLa tecla de la respuesta.

Una respuesta de texto libre que acompaña a las opciones. Al escribir se borra la opción elegida. Dale una etiqueta o aria-label.

PropTipoPredeterminado
variant
Button variant"ghost", or "default" for Next and Submit
size
Button size"default"

QuestionnaireNext y QuestionnaireSubmit comparten un hueco, así que en la última pregunta Next se cambia por Submit sin mover nada.

Usado en bloques

Bloques que se construyen sobre Questionnaire.