XI Conversation

Star on GitHubFeedback
XI ConversationNative HTMLot-quest

Questionnaire

Questions one at a time, set large. The count rolls and a hairline fills; each question turns in from the side you're heading. At the end your answers are written into one sentence, in italic, arriving word by word.

Report an issue with Questionnaire

Example

Sentence

What are you making?

Interview

What are you making?

Definition

What are you making?

Sentence, the end

You’re making a website with a team of two to ten, to launch next season. We’ll reply within two days.

Interview, two asked
  1. What are you making?

  2. How big is your team?

When should it launch?

Interview, one skipped, the last pointed at
  1. What are you making?

  2. How big is your team?

When should it launch?

Definition, the end

websitenoun

the one you’re making with a team of two to ten, to launch next season.

Definition, right to left

موقعاسم

الذي تصنعه مع فريق صغير، ليُطلق في الموسم القادم.

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, button, field, fraction, and radio-group along.

npx shadcn@latest add https://thedirectors.agency/ui/r/questionnaire.json

Usage

questionnaire-example.tsx
"use client"import { Questionnaire, type Answers, type Question } from "@/components/ui/questionnaire"const says: Record<string, Record<string, string>> = {make: { "An identity": "an identity", "A website": "a website", "A film": "a film" },team: { "Just me": "on your own", "Two to ten": "with a team of two to ten", "More than ten": "with a team of more than ten" },when: { "This season": "this season", "Next season": "next season", "When it’s right": "when it’s right" },}const questions: Question[] = [{ id: "make", question: "What are you making?", options: Object.keys(says.make) },{ id: "team", question: "How big is your team?", options: Object.keys(says.team) },{ id: "when", question: "When should it launch?", options: Object.keys(says.when) },{ id: "more", question: "Anything else we should know?", placeholder: "A sentence is plenty" },]const sentence = ({ make, team, when, more }: Answers) =>[`You’re making ${make ? says.make[make] : "something"}${team ? ` ${says.team[team]}` : ""}${when ? `, to launch ${says.when[when]}` : ""}.`,more ? `You added “${more.trim()}”.` : "","We’ll reply within two days.",].filter(Boolean).join(" ")// The definition's sense, and its usage in quotation marks, as a dictionary gives one.const sense = ({ team, when, more }: Answers) =>[`the one you’re making${team ? ` ${says.team[team]}` : ""}${when ? `, to launch ${says.when[when]}` : ""}.`, more ? `“${more.trim()}”` : ""].filter(Boolean).join(" ")const entry = ({ make }: Answers) => (make ? { word: says.make[make].replace(/^an? /, ""), kind: "noun" } : { word: "something", kind: "pronoun" })const partway: Answers = { make: "A website", team: "Two to ten" }const all: Answers = { ...partway, when: "Next season" }function Caption({ children }: { children: string }) {return <p className="ot-label" style={{ margin: 0 }}>{children}</p>}export default function Example() {return (<div className="grid max-w-[44rem]" style={{ gap: "var(--ot-space-9)" }}><div className="grid gap-(--ot-space-5)"><Caption>Sentence</Caption><Questionnaire questions={questions} sentence={sentence} /></div><div className="grid gap-(--ot-space-5)"><Caption>Interview</Caption><Questionnaire variant="interview" questions={questions} sentence={sentence} /></div><div className="grid gap-(--ot-space-5)"><Caption>Definition</Caption><Questionnaire variant="definition" questions={questions} sentence={sense} entry={entry} /></div></div>)}const wide = { width: "30rem", maxWidth: "100%" }

Contract

  • Underneath: native form.
  • Anatomy: <form class="ot-quest" data-variant="sentence | interview | definition"> holding .ot-quest-head (an ot-fraction count and a filling hairline), in an interview <ol class="ot-quest-log">, .ot-quest-step sections each with a .ot-quest-q question and its controls, .ot-quest-actions (Skip, Next), then .ot-quest-said (aria-live="polite") holding, in a definition, .ot-quest-entry (.ot-quest-word, .ot-quest-kind), and .ot-quest-sentence. defaultAnswers and defaultStep resume one.
  • One question at a time, set large. The count rolls and the hairline fills; each question turns in from the side you're heading (ot-turn). At the end your answers are written into one sentence, arriving word by word, in italic, because every word of it is yours.
  • Interview: after the printed interview's Q and A. Every question asked stays above the next, small and roman, with your answer under it in italic; Q and A hang in the margin in pencil and step in below 860px. Each answer is a button: pressing it goes back to that question (a hairline sketches under it when pointed at). A skipped one says so in pencil. At the end the whole transcript stands over the sentence.
  • Definition: after "the uncreative" (poster VI and its definition). The end is a dictionary entry: the headword (yours, so in the large italic) reversed out of an ink block that draws across behind it from the inline start, the part of speech under it small in roman, then the sense in italic at the lead size, a usage in quotation marks if you wrote one. entry(answers) gives the word and its part of speech. Forced colours: Canvas on CanvasText.
  • Keyboard: native form controls; focus moves to each new question (to the chosen answer when you go back to one).

Props

Props of Questionnaire
PropTypeDefaultWhat it does
variant"sentence" | "interview" | "definition""sentence"interview: every question asked stays above the next, small, with your answer under it, like a printed interview's Q and A; press an answer to go back to it. definition: the end is a dictionary entry, your headword in italic reversed out of an ink block, then the sentence as its sense.
questions{ id: string; question: ReactNode; options?: string[]; label?: string; placeholder?: string }[]In order. With options, the question is a Radio Group and needs a choice to go on. Without, it's an optional free-text line.
sentence(answers: Record<string, string | undefined>) => stringWrites the answers into one sentence. Only questions that were answered are in answers.
onComplete(answers) => voidCalled once when the last question is sent, with the answers.
submitLabelstring"Send answers"The last question's action.
entry(answers) => { word: string; kind: string }Definition only: the headword and its part of speech.
defaultAnswersRecord<string, string | undefined>Answers to start from, to resume a questionnaire.
defaultStepnumber0The question to start at; the number of questions is the end.
errorstring"Choose one to go on, or skip this question."Said when a choice is needed to go on.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
QuestionnaireHealthy habits →Your answers written as one sentence
Interview questionnaireThe printed interview's Q and AWhat's been asked stays as a transcript; press an answer to go back
Definition questionnaire"the uncreative"The end as a dictionary entry, your headword reversed out of ink

How it moves

How it moves
ComponentArticulationWhat moves
QuestionnaireTurnQuestions turn in from where you're heading; the sentence writes in
Interview questionnaireMinutesThe answered pair is written into the transcript as the next question turns in
Definition questionnaireReversalThe ink block draws across and the headword appears in it where it has reached; the sense writes in after