Skip to content

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-builder

Also 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:

surveyBuilder-view.tsx
'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
      }}
    />
  )
}

Reference

Props15 props, 4 types

Props

SurveyBuilder props
PropType
themeThemeInput

Wraps the block in this theme. Default: the theme around it.

–
embeddedboolean

Hosted inside another page: a panel of fixed height instead of the whole screen.

default false

false
copyPartial<SurveyBuilderCopy>

Any of the block's strings, to rewrite or translate.

–
localestring

Formats the reach and dates (BCP 47). Default: the app's locale (YoujProvider), else en-US.

–
valueSurvey

The survey being built, controlled: pair it with onValueChange.

–
defaultValueSurvey

The survey to start from, when the block owns it.

default Tern's product survey

Tern's product survey
onValueChange(survey: Survey) => void

Called with the survey on every change.

–
selectedstring

The question being edited (its id, or 'ending'), controlled: pair it with onSelectedChange, to give each question a route.

–
defaultSelectedstring

The question selected at first, when the block owns it. Default: the first.

–
onSelectedChange(id: string) => void

Called when another question is selected.

–
onSave(survey: Survey) => Promise<unknown> | unknown

Saves 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 | void

Publishes it; resolve with its url to show that link. Reject to stay on the step and say why. Default: a demo wait and a demo link.

–
segmentsSurveySegment[]

The groups an in-app survey can be shown to.

default the demo product's four

the demo product's four
publishedUrlstring

Starts already published at this link (to edit a live survey). Default: a draft.

–
qrCode(url: string) => ReactNode

A real QR code for the share card, from your QR library. Default: QR-like art.

–

Survey

Survey props
PropType
titleRequiredstring–
descriptionstring–
questionsRequiredSurveyQuestion[]–
ending{ title: string; description?: string }

What respondents see once their answers are sent. Default: copy.doneTitle and copy.doneDescription.

–
designSurveyDesign–

SurveyDeployment

SurveyDeployment props
PropType
surveyRequiredSurvey–
channelRequiredSurveyChannel
'link''embed''in-app'
–
slugRequiredstring

The link's last part: forms.tern.so/<slug>.

–
audienceRequired'anyone' | 'members'

Link and embed: anyone with it, or signed-in members only.

–
segmentRequiredstring

In-app: the segment it's shown to, the share of it (1–100), when, and whether a person sees it once.

–
sampleRequirednumber–
triggerRequired'load' | 'idle' | 'exit'–
onceRequiredboolean–
startRequiredstring | null

When it opens: now (null), or a local date and time (2026-10-12T09:00).

–
closeRequired{ kind: 'never' } | { kind: 'date'; date: string } | { kind: 'responses'; responses: number }

When it closes.

–

SurveyPublished

SurveyPublished props
PropType
urlstring–

SurveySegment

SurveySegment props
PropType
idRequiredstring–
labelRequiredstring–
peopleRequirednumber

How 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.

surveystitleLabeldraftlivescheduledchangessavingsavedsaveFailedretryundoredoundoneredonedesignpreviewtestpublishpublishChangessharemorequestionsquestionCountaddQuestionendingendingHintuntitledrowLabelhasLogicbackToQuestionsgroupskindshintsstartersoptionrowmatrixColumnsscaleLowscaleHighquestionOftypetitletitlePlaceholderdescriptiondescriptionPlaceholderanswersoptionsrowscolumnsaddOptionaddRowaddColumnpasteHintoptionLabelremoveOptionreorderOptionsettingsrequiredrequiredHintmultipleAnswersmaxAnswersnoLimitotherOptionotherHintrandomizerandomizeHintlongAnswermaxLengthplaceholderfromtostepunitunitPlaceholderstarsfaceslowLabelhighLabelyesLabelnoLabellogicshowalwayswhenmatchAllmatchAnyaddConditionremoveConditionconditionQuestionconditionOpconditionValuenoEarlierlateropsduplicatemoveUpmoveDowndeletedeletedendingTitleendingDescriptionemptyTitleemptyDescriptionpreviewLabelokrespondentdesktopphonedevicetestTitletestDescriptionlayoutlayoutsdesignTitleaccentaccentInheritaccentLabelcornerscornerNamesfontfontNamespublishTitlepublishDescriptionstepWherestepWhenchannelchannelschannelHintsaddresslinkBaseaudienceaudiencessegmentsamplereachtriggertriggersoncestartstartNowstartLaterstartAtclosecloseNevercloseDatecloseResponsescloseDateLabelresponsesLabelbackcontinuepublishingpublishedpublishFailedliveTitlescheduledTitleliveDescriptionslinkcopyLinkcopiedopenLinkshareCardshareCardHintembedCodecopyCodedone
Built 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

WhereKeysWhat they do
A choiceA, B, C…Pick an option (toggle it in a multiple choice); the next letter is “Other”
A scale, NPS or faces0–9Pick a point; 1 then 0 is ten
Yes or noY, NAnswer
Any questionEnterGo on (in Single page, move to the next question)
A long answer⌘/Ctrl + EnterGo on
A ranking or the outlineSpace, ↑ ↓, SpacePick a row up, move it, drop it (Esc puts it back)
An option in the builderEnter, BackspaceAdd the next option; remove an empty one
The builder⌘/Ctrl + Z, ⇧⌘/Ctrl + ZUndo, redo (outside a text field)
The widgetEscDismiss 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.