Appearance
Questionnaire Implementation Guide
Overview
This guide explains how to implement questionnaires using our structured JSON format. Questionnaires are composed of pages with flexible layouts using rows and columns, containing various interactive and static elements.
Table of content:
Questionnaire flow
Questionnaire flow steps:
- Make a request to show a desired questionnaire in the requested language - Heavy request containing all pages, elements, and layout
- Make a request to get participant's questionnaire progress
- Render the questionnaire - Display all parts of questionnaire structure on the page
- Validate each page - Submit and validate answers page by page. Validating pages stores participant's answers, and they can later be retrieved in get participant's questionnaire progress
- Submit Completed Questionnaire - Final submission when all pages are complete
The reason we decided to split first two requests is that the first request (most heavy one) can be cached, hence it will not be changed that often.
WARNING
If for some reason validating or submitting a page returns a validation error, that cache should be cleared, and you should make a new request to get a questionnaire (making sure you have the latest data).
The questionnaire should be rendered one page at a time, we suggest doing it with a JavaScript implementation, showing it in a single request. Render a questionnaire page, validate it, if the response is successful, show the next one.
The reason we chose this approach is that making one big request to our API is faster than making a separate request for each page.
After all pages have been validated you can make a submit questionnaire request. This request also validates the whole questionnaire, and if there are some errors, they will be returned alongside pages on which they occurred.
Questionnaire progress
From get questionnaire progress endpoint we're providing all the current answers a participant has submitted for requested questionnaire, as well as max_submitted_page_number.
That means that when users completes a part of questionnaire, and exits the app, they could continue later from where they stopped.
Answers are collected as key-value pairs where keys match element slugs, and value match provided user value.
json
{
"message": "Success",
"data": {
"max_submitted_page_number": 1,
"answers": {
"user_gender": "0",
"user_dob_day": "1",
"user_dob_month": "1",
"user_dob_year": "1990",
"user_gender_inclusive": "1",
"weight": "80",
"height": "170"
}
}
}Questionnaire Structure
Hierarchy
Questionnaire
├── Sections (optional)
├── Pages
│ ├── Rows
│ │ ├── Columns
│ │ │ └── Elements (various types)Example questionnaire structure
json
{
"message": "Success",
"data": {
"id": "01jd9wgbena9v0hdbv27yfcmy8",
"slug": "pmo",
"name": "Smart Health Test",
"sections": [
// array of sections. Each section contains id, name
],
"pages": [
// array of pages
],
"pages_count": 20
}
}Page Layout System
Page Structure
json
{
"section_id": "section-1", // could be nullable
"content": [
{
"id": "row-1",
"content": [
// Array of rows
]
}
]
}Row Structure
Rows organize content horizontally across the page.
json
{
"id": "row-1",
"title": "Personal Details", // string or null
"content": [
// Columns with percentage-based widths
]
}What's the role of a row title?
Let's imagine we have a row and several columns inside it. Each column contains one element, and we want to show a single question above multiple columns.
If we add text or content to one of the columns, it will be contained by width to only this column. Adding a separate row might not work either, because the styling would be hard to fit. Here comes row title to the rescue.
Row title should be placed in UI inside row, but above columns, so it could spread across all columns in the row.
As for row title type, it's either string or null.
It should be styled the same way as element's question is.
Example row title usage

If you take a look at this example, you could see that text 'Date of birth' starts where first column starts and continues inside second column.
Column Structure
Columns divide rows into flexible sections with percentage-based widths.
json
{
"id": "column-1",
"width": 50, // Percentage of row width (1-100)
"content": [
// Array of questionnaire elements
]
}Width Rules:
- Sum of all column widths in a row must equal 100
- Minimum width: 1
- Maximum width: 100
Questionnaire Elements
Available Element Types
| Element Type | Description | Key Features |
|---|---|---|
| Select | Dropdown with numeric range | Auto-generated number sequences, customizable prefixes/suffixes |
| SelectText | Dropdown with predefined options | Exclusive choice, custom text options, flexible labeling |
| Radio | Single selection from multiple options | Exclusive choice, clear visual selection |
| Checkbox | Single toggle for boolean values | Simple agreement/acceptance, binary choice |
| MultiCheckbox | Multiple selection checkboxes | Non-exclusive choices, select multiple options |
| Text element | Dynamic content | Additional content beside elements on page, no user input |
Each element type (except Text element) supports common properties like slug, title, helpText, question, preText, and postText for comprehensive content presentation.
WARNING
Please note that questionnaire text element and text inside dynamic content are two distinct things. First one is one of questionnaire elements, while the second one is just a way to render text inside dynamic content.
Common properties
Apart from text element, all other questionnaire elements share these common properties:
json
{
"id": "some-very-unique-id",
"slug": "unique_element_identifier",
"type": "element_type",
"title": "Element title content", // Optional - uses dynamic content format
"helpText": "Help text content", // Optional - uses dynamic content format
"question": "Question text", // String, but can be an empty string, some elements do not have questions
"preText": "Text before element", // Optional - uses dynamic content format
"postText": "Text after element" // Optional - uses dynamic content format
}Dynamic Content Field Types
title,helpText,preText,postText,textuse the dynamic content formatquestionuses plain text for accessibility labels
Visual example of a questionnaire elements with all it's fields:

Element Types
1. Select Element
Dropdown with numeric range values.
Typescript definition:
typescript
type QuestionnaireElementSelectData = {
id: string
slug: string
type: string
question: string
title: DynamicContent | null
helpText: DynamicContent | null
preText: DynamicContent | null
postText: DynamicContent | null
start: number
stop: number
step: number
initialValue: number
unit: string | null
firstItemPrefix: string | null
firstItemSuffix: string | null
lastItemPrefix: string | null
lastItemSuffix: string | null
}TIP
Hereinafter, in the typescript definition, DynamicContent refers to dynamic content.
Example Select element JSON:
json
{
"id": "01je3v6qkke0a4726fkknbrhzf",
"slug": "weight",
"type": "select",
"question": "How much do you weigh?",
"title": null,
"help_text": null,
"pre_text": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "For pregnant/ recently delivered women: enter your pre-pregnancy weight",
"marks": null
}
]
}
]
},
"post_text": null,
"start": 34,
"stop": 151,
"step": 0.1,
"initial_value": 70,
"unit": "kg",
"first_item_prefix": "",
"first_item_suffix": "or less",
"last_item_prefix": "",
"last_item_suffix": "or more"
}Properties explanation:
start: Starting number of rangestop: Ending number of rangestep: Increment between valuesinitialValue: Default selected valuefirstItemPrefix: Text before first optionlastItemPrefix: Text before last optionfirstItemSuffix: Text after first optionlastItemSuffix: Text after last optionunit: Unit text appended to all values
Generated Options: Creates numeric range from start to stop with step increments.
Example select element UI:

2. SelectText Element
Dropdown with predefined text options.
Typescript definition:
typescript
type QuestionnaireElementSelectTextData = {
id: string
slug: string
type: string
question: string
title: DynamicContent | null
helpText: DynamicContent | null
preText: DynamicContent | null
postText: DynamicContent | null
options: {
label: string
value: string
}[]
}Example SelectText element JSON:
json
{
"id": "01je42rzc2rnp7qymagd1cyeym",
"slug": "ls_exercise_physical_activity_muscle_bone",
"type": "select_text",
"question": "How many days in the past week did you perform muscle and bone strengthening activities?",
"title": {
"type": "doc",
"content": []
},
"help_text": {
"type": "doc",
"content": []
},
"pre_text": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Muscle- and bone-strengthening activities are activities that strain your muscles and challenge your fitness. Examples: working out, running, playing football, playing tennis, climbing stairs, dancing, tilling the garden. Include only the activities you did for at least 10 minutes at a time.",
"marks": null
}
]
}
]
},
"post_text": {
"type": "doc",
"content": []
},
"options": [
{
"label": "0 days",
"value": "0"
},
{
"label": "1 day",
"value": "1"
},
{
"label": "2 days",
"value": "2"
},
{
"label": "3 days",
"value": "3"
},
{
"label": "4 days",
"value": "4"
},
{
"label": "5 days",
"value": "5"
},
{
"label": "6 days",
"value": "6"
},
{
"label": "7 days",
"value": "7"
}
]
}Properties explanation:
options: Array of selectable optionsvalue: Internal identifierlabel: Display text
Example SelectText element UI

3. Radio Element
Single selection from multiple options.
Typescript definition:
typescript
type QuestionnaireElementRadioData = {
id: string
slug: string
type: string
question: string
title: DynamicContent | null
helpText: DynamicContent | null
preText: DynamicContent | null
postText: DynamicContent | null
options: {
label: string
value: string
}[]
}Example Radio element JSON:
json
{
"id": "01je431myfysj8wtn2hngtwvre",
"slug": "ls_exercise_physical_activity_balance",
"type": "radio",
"question": "Do you combine this with balance exercises (examples: yoga, pilates and exercises with a balance ball)?",
"title": {
"type": "doc",
"content": []
},
"help_text": {
"type": "doc",
"content": []
},
"pre_text": {
"type": "doc",
"content": []
},
"post_text": {
"type": "doc",
"content": []
},
"options": [
{
"label": "No",
"value": "0"
},
{
"label": "Yes",
"value": "1"
}
]
}Properties explanation:
options: Array of radio button choices. Each option represents separate htmllabelwithinput type="radio"elementvalue: Internal identifier of optionlabel: Display text of option
Example Radio element UI

4. Checkbox Element
Single checkbox for boolean values.
Type definition:
typescript
type QuestionnaireElementCheckboxData = {
id: string
slug: string
type: string
question: string
title: DynamicContent | null
helpText: DynamicContent | null
preText: DynamicContent | null
postText: DynamicContent | null
checkboxText: string | null
}Example Checkbox element JSON:
json
{
"id": "01je431myfysj8wtn2hngtwvre",
"slug": "terms_checkbox",
"type": "checkbox",
"question": "",
"title": null,
"helpText": null,
"preText": null,
"postText": null,
"checkboxText": "I agree to the terms and conditions"
}Properties explanantion:
checkboxText: Label text for the checkbox
WARNING
Unlike default checkbox input inside html form, you need to set send checkbox value as "1" when it's checked, and "0" when unchecked. E.g.
json
{
"terms_checkbox": "0"
// other Questionnaire progress items
}would let know the system know that the user didn't check terms_checkbox checkbox, while "1" that they checked it.
Example Checkbox element UI

5. MultiCheckbox Element
Multiple selectable checkboxes.
Typescript definition:
typescript
type QuestionnaireElementMultiCheckboxData = {
id: string
slug: string
type: string
question: string
title: DynamicContent | null
helpText: DynamicContent | null
preText: DynamicContent | null
postText: DynamicContent | null
options: {
label: string
value: string
}[]
}Example MultiCheckbox element JSON:
json
{
"id": "01je431myfysj8wtn2hngevunp",
"slug": "asr_details_children",
"type": "multi_checkbox",
"question": "If you have children, what age category does your child(ren) fall into? (multiple answers possible)",
"title": {
"type": "doc",
"content": []
},
"helpText": {
"type": "doc",
"content": []
},
"preText": {
"type": "doc",
"content": []
},
"postText": {
"type": "doc",
"content": []
},
"options": [
{
"label": "0-12 years",
"value": "0"
},
{
"label": "13-21 years",
"value": "1"
},
{
"label": "22 years and older",
"value": "2"
},
{
"label": "No children",
"value": "3"
}
]
}TIP
For each option inside options field, you need to create separate input checkbox. Each checkbox name attribute should use format {slug}_{value}. For example above, asr_details_children_0, where asr_details_children is slug of element, and 0 is value of one of the options.
Properties explanation:
options: Array of checkbox optionsvalue: Internal identifierlabel: Display text
WARNING
Unlike default checkbox input inside html form, you need to set send checkbox value as "1" when it's checked, and "0" when unchecked.
Given example above, if user checked only options for '0-12 years' and '13-21 years', questionnaire progress should look like this:
json
{
"asr_details_children_0": "1",
"asr_details_children_1": "1",
"asr_details_children_2": "0",
"asr_details_children_3": "0"
// other Questionnaire progress items
}Example MultiCheckbox element UI

Checkbox Value Format
- Checked: Value should be
"1" - Unchecked: Value should be
"0"
This applies to both:
- Checkbox Element (single checkbox)
- MultiCheckbox Element (each individual checkbox)
6. Text Element
Static content display without interaction.
Typescript definition:
typescript
type TextElementData = {
text: EditorData
}Example Text element JSON:
json
{
"type": "text",
"text": {
"type": "doc",
"content": [
{
"type": "heading",
"content": [
{
"type": "text",
"text": "Body",
"marks": null
}
],
"attrs": {
"level": 1
}
},
{
"type": "paragraph",
"content": null
},
{
"type": "paragraph",
"content": null
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "How much moderate-intensity physical activity did you do in the past week?",
"marks": [
{
"type": "questionStyle"
}
]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Moderate-intensity physical activity causes you to breathe slightly faster than normal. Examples include recreational cycling, brisk walking, gardening, vacuuming, etc.",
"marks": null
}
]
}
]
}
}Properties:
text: Content using dynamic content
Example Text element UI

Questionnaire JSON example
Click to toggle the code
json
{
"id": "01jd9wgbena9v0hdbv27yfcmy8",
"slug": "pmo",
"name": "Smart Health Test",
"sections": [
{
"id": "personal",
"name": "Personal Information"
}
],
"pages": [
{
"section_id": null,
"id": "01JJ9N0CTZ5FBFFJJHWE0VJ1XG",
"content": [
{
"type": "row",
"content": [
{
"type": "column",
"width": 100,
"content": [
{
"id": "01jd9thv0rjsf9xxnshj06vjkt",
"slug": "user_gender",
"type": "radio",
"question": "What sex were you assigned at birth?",
"title": {
"type": "doc",
"content": [
{
"type": "heading",
"content": [
{
"type": "text",
"text": "Basic information",
"marks": null
}
],
"attrs": {
"level": 1
}
},
{
"type": "paragraph",
"content": null
},
{
"type": "paragraph",
"content": null
},
{
"type": "paragraph",
"content": null
},
{
"type": "paragraph",
"content": null
}
]
},
"help_text": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "You can read ",
"marks": null
},
{
"type": "text",
"text": "here",
"marks": [
{
"type": "link",
"attrs": {
"rel": "noopener noreferrer nofollow",
"href": "https://app.smarthealth.works/articles/1772640463",
"class": null,
"target": "_blank"
}
}
]
},
{
"type": "text",
"text": " why we ask for your sex at birth, gender identity and date of birth.",
"marks": null
}
]
}
]
},
"pre_text": {
"type": "doc",
"content": []
},
"post_text": {
"type": "doc",
"content": []
},
"options": [
{
"label": "Female",
"value": "0"
},
{
"label": "Male",
"value": "1"
}
]
},
{
"type": "text",
"text": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "aaaa",
"marks": null
}
]
}
]
}
}
]
}
],
"title": null
},
{
"type": "row",
"content": [
{
"type": "column",
"width": 100,
"content": [
{
"id": "01jdmmtyabjd5spg3m56h9ec1n",
"slug": "user_gender_inclusive",
"type": "radio",
"question": "What gender do you identify with?",
"title": null,
"help_text": null,
"pre_text": null,
"post_text": null,
"options": [
{
"label": "Female",
"value": "0"
},
{
"label": "Male",
"value": "1"
},
{
"label": "Female-to-male/transgender male/transman",
"value": "2"
},
{
"label": "Male-to-female/transgender woman/transwoman",
"value": "3"
},
{
"label": "Gender queer/ neither exclusively male nor female",
"value": "4"
},
{
"label": "Additional gender category/other",
"value": "5"
},
{
"label": "Choose not to answer",
"value": "6"
}
]
}
]
}
],
"title": null
},
{
"type": "row",
"content": [
{
"type": "column",
"width": 10,
"content": [
{
"id": "01jd9thv11wfgk56nzrahy8edf",
"slug": "user_dob_day",
"type": "select",
"question": "",
"title": {
"type": "doc",
"content": []
},
"help_text": {
"type": "doc",
"content": []
},
"pre_text": {
"type": "doc",
"content": []
},
"post_text": {
"type": "doc",
"content": []
},
"start": 1,
"stop": 31,
"step": 1,
"initial_value": 1,
"unit": "",
"first_item_prefix": "",
"first_item_suffix": "",
"last_item_prefix": "",
"last_item_suffix": ""
}
]
},
{
"type": "column",
"width": 10,
"content": [
{
"id": "01jd9thv18a3nxv9zgennd6n8z",
"slug": "user_dob_month",
"type": "select_text",
"question": "",
"title": {
"type": "doc",
"content": []
},
"help_text": {
"type": "doc",
"content": []
},
"pre_text": {
"type": "doc",
"content": []
},
"post_text": {
"type": "doc",
"content": []
},
"options": [
{
"label": "January",
"value": "1"
},
{
"label": "February",
"value": "2"
},
{
"label": "March",
"value": "3"
},
{
"label": "April",
"value": "4"
},
{
"label": "May",
"value": "5"
},
{
"label": "June",
"value": "6"
},
{
"label": "July",
"value": "7"
},
{
"label": "August",
"value": "8"
},
{
"label": "September",
"value": "9"
},
{
"label": "October",
"value": "10"
},
{
"label": "November",
"value": "11"
},
{
"label": "December",
"value": "12"
}
]
}
]
},
{
"type": "column",
"width": 10,
"content": [
{
"id": "01jd9thv1fp5ab6vpcthghrwq7",
"slug": "user_dob_year",
"type": "select",
"question": "",
"title": {
"type": "doc",
"content": []
},
"help_text": {
"type": "doc",
"content": []
},
"pre_text": {
"type": "doc",
"content": []
},
"post_text": {
"type": "doc",
"content": []
},
"start": 1930,
"stop": 2019,
"step": 1,
"initial_value": 1985,
"unit": "",
"first_item_prefix": "",
"first_item_suffix": "",
"last_item_prefix": "",
"last_item_suffix": ""
}
]
},
{
"type": "column",
"width": 38,
"content": []
},
{
"type": "column",
"width": 16,
"content": []
},
{
"type": "column",
"width": 16,
"content": []
}
],
"title": "Date of birth"
},
{
"type": "row",
"content": [
{
"type": "column",
"width": 100,
"content": [
{
"id": "01je3v6qkke0a4726fkknbrhzf",
"slug": "weight",
"type": "select",
"question": "How much do you weigh?",
"title": {
"type": "doc",
"content": []
},
"help_text": {
"type": "doc",
"content": []
},
"pre_text": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "For pregnant/ recently delivered women: enter your pre-pregnancy weight",
"marks": null
}
]
}
]
},
"post_text": {
"type": "doc",
"content": []
},
"start": 34,
"stop": 151,
"step": 0.1,
"initial_value": 70,
"unit": "kg",
"first_item_prefix": "",
"first_item_suffix": "or less",
"last_item_prefix": "",
"last_item_suffix": "or more"
}
]
}
],
"title": ""
},
{
"type": "row",
"content": [
{
"type": "column",
"width": 100,
"content": [
{
"id": "01je3vxpezxm5akr76m6zhn8c4",
"slug": "height",
"type": "select",
"question": "What's your height?",
"title": {
"type": "doc",
"content": []
},
"help_text": {
"type": "doc",
"content": []
},
"pre_text": {
"type": "doc",
"content": []
},
"post_text": {
"type": "doc",
"content": []
},
"start": 139,
"stop": 211,
"step": 1,
"initial_value": 170,
"unit": "cm",
"first_item_prefix": "",
"first_item_suffix": "or less",
"last_item_prefix": "",
"last_item_suffix": "or more"
}
]
}
],
"title": ""
}
]
},
// other pages
]
}Implementation Guidelines
Rendering Flow
- Process sections - Display section headers when available
- Render pages - Show one page at a time in sequence
- Layout rows - Arrange content horizontally
- Size columns - Use percentage widths for responsive layout
- Render elements - Handle each element type appropriately
Validation
1. Front end validation Validation
Implementing front end validation is not mandatory, but if you wish so here are some guidelines:
- All elements are required at the moment
Radio,Select text,Selectelements must have a valid selection- Checkbox and MultiCheckbox elements must have value
"1"or"0"(see)
2. Validate page endpoint Validation
Complete page validation before progression:
json
{
"errors": {
"resilience_achieve_goals": ["Resilience_achieve_goals is required."],
"resilience_bounce_back": ["Resilience_bounce_back is required."]
}
}Display Guidelines:
- Error messages shown below problematic fields
- Visual indicators (red borders, icons)
- Maintains user's entered data
- Scrolls to first error field when possible
3. Errors on submit questionnaire
Page-level errors are only returned during final questionnaire submission and use this format:
json
{
"errors": {
"questionnaire_page.0": ["This page has errors."],
"answers.user_gender": ["Provided value for user_gender is not valid"]
}
}Important: The number after questionnaire_page. represents the zero-based index of the page where the error occurred. For example:
questionnaire_page.0= Error on first pagequestionnaire_page.1= Error on second page
Display Guidelines:
- General error message at page top/bottom
- Summarizes multiple field issues
- Prevents final submission until resolved
We recommend to redirect users to the first page where an error happened, so they could fix the error, and continue with questionnaire.
Answer State Management
We recommend maintaining all user answers in local state and updating them as user interacts with questionnaire elements:
javascript
const answers = {
user_gender: '0',
user_dob_day: '1',
user_dob_month: '1',
user_dob_year: '1990',
user_gender_inclusive: '1',
weight: '80',
height: '170',
}Error Recovery Flow
- User submits page with errors
- System displays specific error messages
- User corrects invalid fields
- System re-validates on next submission attempt
- Progress allowed when all errors resolved
Error State Management
typescript
type ValidationErrors = Record<string, string[]>
type ValidationState = {
errors: ValidationErrors
isPageError: boolean
errorMessage: string
hasErrors: boolean
}
const validationState = {
hasErrors: false, // if state has an error
isPageError: false, // if error is page level
errors: {}, // object with key as questionnaire element slug, and value as array of strings
errorMessage: '', // error message at the top/bottom of the page
}User Experience guidelines
- Clear Messaging: Specific, actionable error descriptions
- Visual Feedback: Consistent error styling across all elements
- Accessibility: Screen reader announcements for errors
- Focus Management: Automatic scrolling to first error field (optional)