Skip to content

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:

  1. Make a request to show a desired questionnaire in the requested language - Heavy request containing all pages, elements, and layout
  2. Make a request to get participant's questionnaire progress
  3. Render the questionnaire - Display all parts of questionnaire structure on the page
  4. 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
  5. 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

Row title example in UI

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 TypeDescriptionKey Features
SelectDropdown with numeric rangeAuto-generated number sequences, customizable prefixes/suffixes
SelectTextDropdown with predefined optionsExclusive choice, custom text options, flexible labeling
RadioSingle selection from multiple optionsExclusive choice, clear visual selection
CheckboxSingle toggle for boolean valuesSimple agreement/acceptance, binary choice
MultiCheckboxMultiple selection checkboxesNon-exclusive choices, select multiple options
Text elementDynamic contentAdditional 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, text use the dynamic content format
  • question uses plain text for accessibility labels

Visual example of a questionnaire elements with all it's fields:

Row title example in UI

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 range
  • stop: Ending number of range
  • step: Increment between values
  • initialValue: Default selected value
  • firstItemPrefix: Text before first option
  • lastItemPrefix: Text before last option
  • firstItemSuffix: Text after first option
  • lastItemSuffix: Text after last option
  • unit: Unit text appended to all values

Generated Options: Creates numeric range from start to stop with step increments.

Example select element UI:

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 options
  • value: Internal identifier
  • label: Display text

Example SelectText element UI

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 html label with input type="radio" element
  • value: Internal identifier of option
  • label: Display text of option

Example Radio element UI

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

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 options
  • value: Internal identifier
  • label: 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

MultiCheckbox element UI

Checkbox Value Format

  • Checked: Value should be "1"
  • Unchecked: Value should be "0"

This applies to both:

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:

Example Text element UI

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

  1. Process sections - Display section headers when available
  2. Render pages - Show one page at a time in sequence
  3. Layout rows - Arrange content horizontally
  4. Size columns - Use percentage widths for responsive layout
  5. 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, Select elements 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 page
  • questionnaire_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

  1. User submits page with errors
  2. System displays specific error messages
  3. User corrects invalid fields
  4. System re-validates on next submission attempt
  5. 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)