Surveys
Both sides of a survey: the builder where it's written, branched, styled and published, and the ways people answer it (one question at a time, a single page, a deck of cards for the phone, a widget in your product), all from one survey definition, with its results at a glance.
Installation & usage
npx @youj/cli add surveys-builderAlso installs async-button, badge, button, dialog, dropdown-menu, empty, field, icon-align-left, icon-arrow-down, icon-arrow-left, icon-arrow-up, icon-at-sign, icon-calendar, icon-check, icon-chevron-left, icon-circle-alert, icon-circle-dot, icon-cloud-off, icon-code, icon-copy, icon-copy-plus, icon-external-link, icon-flag, icon-gauge, icon-git-branch, icon-hash, icon-layout-grid, icon-link, icon-list-ordered, icon-monitor, icon-more-horizontal, icon-palette, icon-panel-bottom, icon-play, icon-plus, icon-redo, icon-rocket, icon-sliders, icon-smartphone, icon-smile, icon-square-check, icon-star, icon-thumbs-up, icon-trash, icon-type, icon-undo, icon-x, input, input-group, kbd, morph-label, number-input, popover, presence-list, progress, radio-group, segmented-control, select, slider, sortable-list, spinner, surveys-conversational, surveys-deck, surveys-form, surveys-question, switch, textarea, toast, tooltip, youj-core, youj-presets, youj-ui-core.
The CLI copies the block to blocks/surveys/builder.tsx with every component it uses, so it’s yours to edit.
Every prop is optional and falls back to the demo, so it renders as you see it until you pass your own. Give it your data, rewrite or translate its strings through copy, and connect its actions:
'use client'
import { SurveyBuilder, type SurveyBuilderProps } from '@/blocks/surveys/builder'
export function SurveyBuilderView({ value, segments }: Pick<SurveyBuilderProps, 'value' | 'segments'>) {
return (
<SurveyBuilder
value={value}
segments={segments}
copy={{ surveys: 'Surveys' }}
onValueChange={(survey) => {
// your code
}}
onSelectedChange={(id) => {
// your code
}}
onSave={async (survey) => {
// await your API here; the block shows progress until it settles
}}
/>
)
}Description
The admin's side. The outline holds every question in order: drag a row by its grip, or Tab to the grips and move one with Space and the arrows. Add question opens a palette of fourteen kinds, from single and multiple choice to NPS, a star rating, faces, a slider, a matrix and a ranking, and the new one lands after the selected question. The middle edits it in place: its kind, its words, its answers (paste a list to make options), whether it's required, and show-if logic on earlier answers. The preview beside it is the real question on a phone or a desktop, in the survey's design. Every change is a step of ⌘Z and saves itself a beat later. Publish picks a link, an embed or in-app (with its audience and the share of it asked), when it opens and closes, and ends on the live link with a share card. Keep the survey with value and onValueChange or onSave, publish through onPublish.
One question at a time, as fast as the keyboard: a letter picks a choice, a digit a point on a scale or a face, Y or N answers yes or no, and Enter goes on. A pick on a single-answer question goes on by itself after a beat. It opens on a welcome, each question fills the screen in large type, and the bar at the top counts only the questions this person's answers lead to. It ends on a review of every answer, each with an Edit that comes back to the review, then the thanks. On a phone the action sits at the foot of the screen, in the thumb's reach.
Every question on one page, numbered, the way people fill in a form they can see the end of. A question an answer reveals rises into its place and the rest are renumbered; the header counts what's answered. Enter moves on to the next question. Send checks every answer, says each problem under its question and gathers them in a summary at the top that takes focus and links to each one. The questions scroll under a header that stays, so the page never jumps.
For the phone in your hand: a deck of question cards with answers as large as a thumb, the cards still ahead peeking out below the one on top. Throw the card towards the reading start to go on and the other way to go back, or use the two buttons where the thumb rests. A throw that can't go on springs back and shakes, and the reason is said under the question. Progress fills in segments along the top, one per question this person will be asked.
A short survey in a corner of your product, the way in-app research asks: it rises in without taking focus, asks one question at a time and keeps one size while it does, and thanks you in place. Minimize folds it into a pill that remembers where you were; × and Escape dismiss it and the app hears it through onOpenChange. On a phone it's a sheet along the bottom edge.
What a survey heard: responses, completion and the median time, then any question's answers in the shape its type calls for, bars for choices and ratings, the average place for a ranking, NPS as its score over the split of detractors, passives and promoters, with every score drawn out. It's computed from the raw responses, so pass them as they're stored; moving between questions keeps the card's size.
One question anywhere: a rating under an article, a poll, a check-in after a chat, in any of the fourteen kinds. A pick that needs nothing more sends itself and the thanks takes its place. Its folder is the kit the other blocks share: the survey definition and its show-if logic, validation, and the question renderers, so a survey answers the same way in every layout.
Code
Builder, the Builder story: the files youj add writes.
blocks/surveys
Reference
Props15 props, 4 types
Props
| Prop | Type | |
|---|---|---|
theme | ThemeInputWraps the block in this theme. Default: the theme around it. | – |
embedded | booleanHosted inside another page: a panel of fixed height instead of the whole screen. default false | false |
copy | Partial<SurveyBuilderCopy>Any of the block's strings, to rewrite or translate. | – |
locale | stringFormats the reach and dates (BCP 47). Default: the app's locale (YoujProvider), else en-US. | – |
value | SurveyThe survey being built, controlled: pair it with | – |
defaultValue | SurveyThe survey to start from, when the block owns it. default Tern's product survey | Tern's product survey |
onValueChange | (survey: Survey) => voidCalled with the survey on every change. | – |
selected | stringThe question being edited (its id, or | – |
defaultSelected | stringThe question selected at first, when the block owns it. Default: the first. | – |
onSelectedChange | (id: string) => voidCalled when another question is selected. | – |
onSave | (survey: Survey) => Promise<unknown> | unknownSaves the survey, a beat after the last change; reject to show "Couldn’t save" with Retry. Default: a short demo wait. | – |
onPublish | (deployment: SurveyDeployment) => Promise<SurveyPublished | void> | SurveyPublished | voidPublishes it; resolve with its | – |
segments | SurveySegment[]The groups an in-app survey can be shown to. default the demo product's four | the demo product's four |
publishedUrl | stringStarts already published at this link (to edit a live survey). Default: a draft. | – |
qrCode | (url: string) => ReactNodeA real QR code for the share card, from your QR library. Default: QR-like art. | – |
Survey
| Prop | Type | |
|---|---|---|
titleRequired | string | – |
description | string | – |
questionsRequired | SurveyQuestion[] | – |
ending | { title: string; description?: string }What respondents see once their answers are sent. Default: copy.doneTitle and copy.doneDescription. | – |
design | SurveyDesign | – |
SurveyDeployment
| Prop | Type | |
|---|---|---|
surveyRequired | Survey | – |
channelRequired | SurveyChannel'link''embed''in-app' | – |
slugRequired | stringThe link's last part: forms.tern.so/<slug>. | – |
audienceRequired | 'anyone' | 'members'Link and embed: anyone with it, or signed-in members only. | – |
segmentRequired | stringIn-app: the segment it's shown to, the share of it (1–100), when, and whether a person sees it once. | – |
sampleRequired | number | – |
triggerRequired | 'load' | 'idle' | 'exit' | – |
onceRequired | boolean | – |
startRequired | string | nullWhen it opens: now (null), or a local date and time ( | – |
closeRequired | { kind: 'never' } | { kind: 'date'; date: string } | { kind: 'responses'; responses: number }When it closes. | – |
SurveyPublished
| Prop | Type | |
|---|---|---|
url | string | – |
SurveySegment
| Prop | Type | |
|---|---|---|
idRequired | string | – |
labelRequired | string | – |
peopleRequired | numberHow many people it holds, for the reach estimate. | – |
Strings165 keys for copy
Strings
copy takes any of these keys; the rest keep their defaults. A key whose text holds a value (a count, a name) takes a function.
surveystitleLabeldraftlivescheduledchangessavingsavedsaveFailedretryundoredoundoneredonedesignpreviewtestpublishpublishChangessharemorequestionsquestionCountaddQuestionendingendingHintuntitledrowLabelhasLogicbackToQuestionsgroupskindshintsstartersoptionrowmatrixColumnsscaleLowscaleHighquestionOftypetitletitlePlaceholderdescriptiondescriptionPlaceholderanswersoptionsrowscolumnsaddOptionaddRowaddColumnpasteHintoptionLabelremoveOptionreorderOptionsettingsrequiredrequiredHintmultipleAnswersmaxAnswersnoLimitotherOptionotherHintrandomizerandomizeHintlongAnswermaxLengthplaceholderfromtostepunitunitPlaceholderstarsfaceslowLabelhighLabelyesLabelnoLabellogicshowalwayswhenmatchAllmatchAnyaddConditionremoveConditionconditionQuestionconditionOpconditionValuenoEarlierlateropsduplicatemoveUpmoveDowndeletedeletedendingTitleendingDescriptionemptyTitleemptyDescriptionpreviewLabelokrespondentdesktopphonedevicetestTitletestDescriptionlayoutlayoutsdesignTitleaccentaccentInheritaccentLabelcornerscornerNamesfontfontNamespublishTitlepublishDescriptionstepWherestepWhenchannelchannelschannelHintsaddresslinkBaseaudienceaudiencessegmentsamplereachtriggertriggersoncestartstartNowstartLaterstartAtclosecloseNevercloseDatecloseResponsescloseDateLabelresponsesLabelbackcontinuepublishingpublishedpublishFailedliveTitlescheduledTitleliveDescriptionslinkcopyLinkcopiedopenLinkshareCardshareCardHintembedCodecopyCodedoneBuilt from24 components, 4 blocks
The survey definitionone JSON shape, show-if logic, fourteen kinds
The survey definition
Every block here reads the same Survey: a title, a description, its questions, an ending and an optional design. It's plain data, so it can be stored as JSON and sent anywhere. A question has an id, a type and a title, may be optional (required: false) and may carry showIf: conditions on earlier answers (is, isNot, includes, excludes, lte, gte, answered, skipped), matched all or any. A condition only reads a question that comes earlier and was itself asked, so the count and the progress always follow this person's path. The types are choice (single or multiple, with max, other and randomize), text (short or long, with maxLength), email, nps, scale, rating, emoji, slider, matrix, ranking, yesno and date. Answers are keyed by question id: an option's value, a list of them, a number, true or false, text, a date as YYYY-MM-DD, or a matrix's row → column.
Keyboardanswering, the builder, the deck
Keyboard
| Where | Keys | What they do |
|---|---|---|
| A choice | A, B, C… | Pick an option (toggle it in a multiple choice); the next letter is “Other” |
| A scale, NPS or faces | 0–9 | Pick a point; 1 then 0 is ten |
| Yes or no | Y, N | Answer |
| Any question | Enter | Go on (in Single page, move to the next question) |
| A long answer | ⌘/Ctrl + Enter | Go on |
| A ranking or the outline | Space, ↑ ↓, Space | Pick a row up, move it, drop it (Esc puts it back) |
| An option in the builder | Enter, Backspace | Add the next option; remove an empty one |
| The builder | ⌘/Ctrl + Z, ⇧⌘/Ctrl + Z | Undo, redo (outside a text field) |
| The widget | Esc | Dismiss it |
Accessibilitylabelled groups, focus, live regions, drag alternatives, motion
Accessibility
Each question is named by its title and described by its description and its problem: a choice is a radio group or a group of checkboxes, a scale and the faces are radio groups, each matrix row is a radio group named by its statement, a ranking is a sortable list, and a slider says its value with its unit. Shortcuts only work while focus is on the question's answers, never in a text field or with a modifier key, and arrow keys move a pick without going on. Focus moves to each new question (its field, or its heading) and a polite status says which question it is; the review's Edit buttons name the question they change. Single page's summary takes focus and links to each problem. The deck's throws have buttons beside them, and the widget never takes focus when it appears.
In the builder every reorder has a keyboard twin with each step announced, every icon button is named (with its shortcut in a tooltip), undo and redo are announced, a deleted question offers Undo in a toast, and the publish steps are labelled with focus moved into each. Under reduced motion questions and steps swap in place, the deck's cards cross-fade, the widget appears and folds without travelling, and the results are drawn at their values.