Forms Reference
ODIN Forms 1.0, Declarative Form Layout for Print and Screen
Overview
ODIN Forms defines a schema for declarative form layouts that render faithfully to both print (PDF) and screen (HTML/CSS). A form definition is a standard ODIN document conforming to this schema, the same parser, the same syntax, the same tooling.
ODIN Forms is print-first: every element has absolute positioning on a fixed page. Screen rendering is a convenience layer, not the primary target. This makes it ideal for government forms, regulatory filings, and any context where paper output must match a canonical layout.
Forms are standard ODIN documents. They use the same {section} notation,
the same type sigils, the same path references. A form renderer is a specialized ODIN reader
that interprets the well-known element types defined in this schema.
Design Principles
- Print-first: Fixed page dimensions, absolute coordinates
- Self-describing: Element types in path structure
- HTML5/SVG naming: Familiar attribute names reduce learning curve
- Bidirectional binding: Forms bind to ODIN data documents
- Validation inline: Field constraints live with field definitions
- Composable: Reuse common element definitions
- Accessible: Screen reader support for PDF (Tagged PDF) and HTML (ARIA)
Why Print-First?
Most form libraries optimize for responsive web layouts and treat PDF as an export afterthought. ODIN Forms inverts this: the canonical representation is a fixed page with absolute coordinates. When the output is print, everything simplifies, no responsive negotiation, no flow layout, no text reflow. Fields are rectangles at coordinates.
Screen rendering becomes straightforward: map the page to a viewport, scale uniformly, position elements absolutely. The web version is a faithful preview of the printed output.
Why Absolute Positioning?
Government forms, insurance applications, and regulatory filings have precise layout requirements. Field 12a must be at exactly (2.5", 4.75") with dimensions (1.5" × 0.25"). Relative or flow layouts cannot guarantee this. Absolute positioning means pixel-perfect PDF output, form overlays on scanned documents, predictable field locations for OCR/scanning, and no layout engine variations between renderers.
Document Structure
An ODIN Forms document has six top-level sections:
| Section | Key | Required | Description |
|---|---|---|---|
| Metadata | {$} | Yes | Document-level settings: version, id, title, lang |
| Localization | {$.i18n} | No | Multi-language labels by language code |
| Page defaults | {$.page} | Yes | Default page dimensions, unit, and margins |
| Screen settings | {$.screen} | No | Screen rendering options |
| Pages | {page[n]} | Yes | Array of page definitions with elements |
{$}
odin = "1.0.0"
forms = "1.0.0"
title = "Application for Insurance"
id = "form_auto_app_v1"
lang = "en"
{$.i18n}
en.field_ssn = "Social Security Number"
en.field_dob = "Date of Birth"
es.field_ssn = "Número de Seguro Social"
es.field_dob = "Fecha de Nacimiento"
{$.page}
width = #8.5
height = #11
unit = "inch"
{$.screen}
scale = #1.0
{page[0]}
{.text.title}
; ... page 0 elements
{page[1]}
{.field.employer}
; ... page 1 elementsMetadata
The {$} section declares document identity and version. These fields
are required in every ODIN Forms document.
| Property | Type | Description |
|---|---|---|
odin | version | ODIN specification version (1.0.0) |
forms | version | ODIN Forms schema version (1.0.0) |
title | string | Human-readable form title |
id | string | Unique form identifier (reverse-domain or slug) |
lang | string | Default language code (e.g., en, es) |
version | string | Form version string (optional) |
Localization
Store translations under {$.i18n} with language codes as path prefixes.
Field labels can reference i18n keys directly, or renderers can resolve labels at render
time based on $.lang.
{$.i18n}
en.title = "Personal Auto Application"
en.section1 = "Applicant Information"
en.field_name = "Full Legal Name"
es.title = "Solicitud de Auto Personal"
es.section1 = "Información del Solicitante"
es.field_name = "Nombre Legal Completo"
; Reference from a field label:
{.field.name}
label = @$.i18n.en.field_namePage Defaults
The {$.page} section sets dimensions that apply to all pages. Every page
inherits these values unless overridden.
| Property | Type | Description |
|---|---|---|
width | number | Page width in declared unit |
height | number | Page height in declared unit |
unit | enum | Unit of measure: inch, cm, mm, pt |
margin.top | number | Top margin |
margin.right | number | Right margin |
margin.bottom | number | Bottom margin |
margin.left | number | Left margin |
{$.page}
width = #8.5
height = #11
unit = "inch"
margin.top = #0.5
margin.right = #0.5
margin.bottom = #0.5
margin.left = #0.5Screen Settings
Optional settings that control how the form renders on screen. These have no effect on print or PDF output.
| Property | Type | Description |
|---|---|---|
$.screen.scale | number | Default zoom factor (1.0 = 100%) |
Coordinate System
All element positions and dimensions use the coordinate system defined by the page defaults. The origin is the top-left corner of each page.
| Property | Description |
|---|---|
| Origin | Top-left of page |
| X-axis | Left to right (increasing X moves right) |
| Y-axis | Top to bottom (increasing Y moves down) |
| Unit | Declared in $.page.unit |
Supported Units
| Unit | Identifier | Notes |
|---|---|---|
| Inch | inch | Standard US paper measurement |
| Centimeter | cm | Metric paper measurement |
| Millimeter | mm | Metric, fine-grained positioning |
| Point | pt | 72 points per inch (typographic) |
All coordinates and dimensions within a document use the same declared unit. A document cannot mix units, convert at document level, not field level.
Reusable Types
ODIN Forms defines shared type definitions using the @ path prefix. These are
composed into element definitions using the = @type inheritance syntax. They
are not directly instantiated, they are referenced by element types.
@position
Base positioning for all elements. Required on every positioned element.
{@position}
x = !#
y = !#@dimensions
Width and height for rectangular elements.
{@dimensions}
w = !#
h = !#@stroke
Stroke properties for geometric elements. Uses SVG-compatible attribute names.
{@stroke}
stroke = :/^#[0-9A-Fa-f]{6}$/
stroke-width = #:(0..)
stroke-opacity = #:(0..1)
stroke-dasharray =
stroke-linecap = ("butt", "round", "square")
stroke-linejoin = ("miter", "round", "bevel")@fill
Fill properties for closed shapes.
{@fill}
fill = :/^#[0-9A-Fa-f]{6}$|^none$/
fill-opacity = #:(0..1)@font
Typography properties for text elements.
{@font}
font-family = "Helvetica"
font-size = ##:(1..) ##12
font-weight = ("normal", "bold") "normal"
font-style = ("normal", "italic") "normal"
text-align = ("left", "center", "right") "left"
color = :/^#[0-9A-Fa-f]{6}$/ "#000000"@validation
Field validation constraints. Applied to all field types.
{@validation}
required = ?
pattern =
minLength = ##:(0..)
maxLength = ##:(1..)
min =
max =@binding
Data binding for fields. The bind property is required on all interactive fields.
{@binding}
bind = !@Geometric Elements
Geometric elements define the visual structure of a form, rules, boxes, borders, and decorative shapes. They have no interactive behavior and carry no data. All use SVG-compatible attribute names.
line
A line segment between two points.
| Property | Type | Required | Description |
|---|---|---|---|
x1 | number | Yes | Start X coordinate |
y1 | number | Yes | Start Y coordinate |
x2 | number | Yes | End X coordinate |
y2 | number | Yes | End Y coordinate |
stroke | hex color | No | Line color |
stroke-width | number | No | Line thickness |
{page[0]}
{.line.header_rule}
x1 = #0.5
y1 = #1
x2 = #8
y2 = #1
stroke = "#000000"
stroke-width = #0.5rect
A rectangle. Inherits @position, @dimensions, @stroke, and @fill.
| Property | Type | Required | Description |
|---|---|---|---|
x, y | number | Yes | Top-left corner position |
w, h | number | Yes | Width and height |
rx | number | No | Horizontal corner radius |
ry | number | No | Vertical corner radius |
fill | hex / none | No | Fill color |
stroke | hex color | No | Border color |
{.rect.section_box}
x = #0.5
y = #2
w = #7.5
h = #3
fill = "#f5f5f5"
stroke = "#cccccc"
rx = #0.1
ry = #0.1circle
A circle defined by center point and radius.
| Property | Type | Required | Description |
|---|---|---|---|
cx | number | Yes | Center X coordinate |
cy | number | Yes | Center Y coordinate |
r | number ≥ 0 | Yes | Radius |
ellipse
An ellipse defined by center point and two radii.
| Property | Type | Required | Description |
|---|---|---|---|
cx | number | Yes | Center X coordinate |
cy | number | Yes | Center Y coordinate |
rx | number ≥ 0 | Yes | Horizontal radius |
ry | number ≥ 0 | Yes | Vertical radius |
polygon
A closed shape defined by a series of points. Points format: x1,y1 x2,y2 x3,y3 ...
{.polygon.warning_triangle}
points = "4,0.5 4.5,1.5 3.5,1.5"
fill = "#ffcc00"
stroke = "#cc6600"polyline
An open path defined by points. Same coordinate format as polygon but not closed.
path
An SVG-style path. The d property uses standard SVG path commands: M (move), L (line), C (cubic bezier), Q (quadratic bezier), A (arc), Z (close).
{.path.custom_shape}
d = "M 0.5,1 L 2,1 L 2,2 Z"
stroke = "#000000"
fill = "none"Content Elements
Content elements display static information, labels, images, and machine-readable codes. They are non-interactive but carry accessibility requirements.
text
Static text content. Inherits @position and @font.
| Property | Type | Required | Description |
|---|---|---|---|
x, y | number | Yes | Position (top-left of text baseline) |
content | string | Yes | Text to display. Supports {@odin.*} interpolation. |
w | number | No | Wrap width |
h | number | No | Clip height |
rotate | number | No | Rotation in degrees |
{.text.section_header}
x = #0.5
y = #2.5
content = "Section 1: Applicant Information"
font-family = "Helvetica"
font-size = ##14
font-weight = "bold"img
Embedded image. The src field contains base64-encoded image data with format prefix. The alt field is required for accessibility.
| Property | Type | Required | Description |
|---|---|---|---|
x, y | number | Yes | Position |
w, h | number | Yes | Dimensions |
src | base64 ref | Yes | Base64 image with format prefix (e.g., ^png:...) |
alt | string | Yes | Accessibility description |
background | boolean | No | When true, renders behind all other elements (lowest z-index). Non-interactive in editing tools. |
{.img.logo}
x = #0.5
y = #0.5
w = #1.5
h = #0.5
src = ^png:iVBORw0KGgoAAAANSUhEUgAA...
alt = "Company Logo"
{.img.page_template}
x = #0
y = #0
w = #8.5
h = #11
src = ^webp:UklGRgAAAA...
alt = "Page 1 template"
background = ?truebarcode
1D or 2D barcode. The alt field is required for accessibility, it describes the barcode's purpose for screen readers.
| Property | Type | Required | Description |
|---|---|---|---|
x, y | number | Yes | Position |
w, h | number | Yes | Dimensions |
type | enum | Yes | code39, code128, qr, datamatrix, pdf417 |
content | string | Yes | Data to encode |
alt | string | Yes | Accessibility description |
{.barcode.document_id}
x = #7
y = #0.5
w = #1
h = #1
type = "qr"
content = "DOC-2024-001234"
alt = "Document tracking code DOC-2024-001234"Field Types
Fields are interactive elements that accept user input and bind to data paths. All fields
share a common base structure and inherit @position, @dimensions, @validation, and @binding.
Common Field Properties
| Property | Type | Required | Description |
|---|---|---|---|
type | enum | Yes | Field type: text, checkbox, radio, select, multiselect, date, signature |
label | string | Yes | Visible label and accessibility label |
x, y | number | Yes | Position |
w, h | number | Yes | Dimensions |
bind | path ref | Yes | ODIN path for the field value |
required | boolean | No | Field must have a value on submit |
aria-label | string | No | Override screen reader text when different from visible label |
tabindex | integer ≥ 0 | No | Tab order override |
readonly | boolean | No | Display-only, no user input |
The label field is required and serves dual purpose: visible label for sighted
users and accessibility label for screen readers. Use aria-label to override
the screen reader text when it should differ from the visible label.
Field Values
ODIN Forms supports inline values as the primary data model. Field values are stored
directly on the field element, making the form file self-contained. The bind attribute
declares where the value belongs in a data document, while the value property holds the actual data.
| Field Type | Value Property | Type | Description |
|---|---|---|---|
text | value | string | Current text content |
checkbox | checked | boolean | Whether the checkbox is checked |
radio | value | string | Option value (the selected radio's value is the group's current value) |
select | selected | string | Currently selected option value |
multiselect | selected | string array | Currently selected option values |
date | value | date | Current date value |
signature | value | binary | Captured signature data |
When a separate bound data document is also present, inline values take precedence.
text
Single-line or multiline text input.
| Property | Type | Description |
|---|---|---|
value | string | Current field value |
inputType | enum | Screen rendering hint: text (default), email, tel, password, number, url. Controls the HTML5 input type for mobile keyboards and browser validation. Print output is identical for all types. |
placeholder | string | Hint text when field is empty |
mask | string | Input mask pattern (e.g., ###-##-####) |
multiline | boolean | Allow multiple lines of text |
maxLines | integer | Maximum lines when multiline is true |
{page[0]}
{.field.insured_name}
type = "text"
label = "Insured Name"
value = "John Smith"
x = #0.5
y = #2.5
w = #3
h = #0.3
bind = @policy.insured_name
required = ?true
minLength = ##1
maxLength = ##100
{.field.ssn}
type = "text"
x = #4.3
y = #2.5
w = #2
h = #0.3
label = "Social Security Number"
required = ?true
pattern = "^\d{3}-\d{2}-\d{4}$"
mask = "###-##-####"
bind = @insured.ssncheckbox
Boolean checkbox. Binds to a boolean path, true when checked, false or absent when unchecked.
{.field.agree_terms}
type = "checkbox"
x = #0.5
y = #8
w = #0.2
h = #0.2
label = "I agree to the terms and conditions"
checked = ?true
required = ?true
bind = @application.termsAcceptedradio
Radio button as part of a named group. All radio fields sharing the same group are mutually exclusive. Each radio binds to the same path, when selected, its value is written to that path.
| Property | Type | Required | Description |
|---|---|---|---|
group | string | Yes | Radio group name, same for all options in a set |
value | string | Yes | Value written to bind path when this radio is selected |
{.field.coverage_basic}
type = "radio"
x = #0.5
y = #4
w = #0.2
h = #0.2
label = "Basic Coverage"
group = "coverage"
value = "basic"
bind = @policy.coverageLevel
{.field.coverage_standard}
type = "radio"
x = #0.5
y = #4.3
w = #0.2
h = #0.2
label = "Standard Coverage"
group = "coverage"
value = "standard"
bind = @policy.coverageLevel
{.field.coverage_premium}
type = "radio"
x = #0.5
y = #4.6
w = #0.2
h = #0.2
label = "Premium Coverage"
group = "coverage"
value = "premium"
bind = @policy.coverageLevelselect
Single-selection dropdown.
| Property | Type | Required | Description |
|---|---|---|---|
options | string array | Yes | Array of valid option values |
selected | string | No | Currently selected option value |
placeholder | string | No | Default unselected text |
{.field.state}
type = "select"
x = #4
y = #3
w = #1.5
h = #0.3
label = "State"
required = ?true
bind = @insured.address.state
{.field.state.options[] : ~}
"AL"
"AK"
"AZ"
"AR"
"CA"
"CO"
"CT"
"FL"
"GA"
"NY"
"TX"
; ... remaining statesmultiselect
Multi-selection list. Binds to an array path, the selected values are written as an array.
| Property | Type | Required | Description |
|---|---|---|---|
options | string array | Yes | Array of valid option values |
selected | string array | No | Currently selected option values |
minSelect | integer | No | Minimum number of selections required |
maxSelect | integer | No | Maximum number of selections allowed |
{.field.coverages}
type = "multiselect"
x = #0.5
y = #5
w = #3
h = #1.5
label = "Select Coverages"
required = ?true
minSelect = ##1
maxSelect = ##6
bind = @policy.selectedCoverages
{.field.coverages.options[] : ~}
"liability"
"collision"
"comprehensive"
"uninsured"
"medical"
"rental"date
Date input field. The min and max properties accept ISO 8601
date strings (YYYY-MM-DD).
{.field.dob}
type = "date"
x = #4
y = #2.25
w = #1.5
h = #0.25
label = "Date of Birth"
required = ?true
min = 1900-01-01
max = 2010-01-01
bind = @insured.birthDatesignature
Signature capture area. On screen, renders as a canvas for drawing or typing a signature. On PDF, renders as a signature field. The bound value is a base64-encoded image or structured signature object depending on the renderer.
| Property | Type | Description |
|---|---|---|
date_field | field reference | Associates a date field with this signature for timestamp capture |
{.field.applicant_signature}
type = "signature"
x = #1
y = #9.5
w = #3
h = #0.75
label = "Applicant Signature"
required = ?true
bind = @signatures.applicantData Binding
The bind attribute on each field declares where the field's value belongs in
the output ODIN document. It uses the standard ODIN path reference syntax, the same @path.to.field notation used across all ODIN specifications.
{.field.insured_name}
bind = @policy.insured_name ; top-level path
{.field.street}
bind = @insured.address.street ; nested path
{.field.primary_vehicle_vin}
bind = @policy.vehicles[0].vin ; indexed array pathSubmission Flow
When a form is submitted, the renderer executes this sequence:
- Walk all fields in tab order
- Validate each field against its constraints (
required,pattern,min,max, etc.) - Halt and surface errors if validation fails
- Emit an ODIN document with each field's value placed at its bound path
The form definition and data schema are separate but linked, the form knows where data goes, the schema knows what data is valid. Submission produces a plain ODIN document that can be validated, signed, transformed, and stored independently.
Relative Binding in Regions
Inside a region that binds to an array, child field bindings use @. to reference the current array item. The renderer resolves the full path automatically.
{.region.drivers}
bind = @policy.drivers ; array of driver objects
{.region.drivers.field.name}
bind = @.name ; resolves to @policy.drivers[n].name
{.region.drivers.field.dob}
bind = @.dateOfBirth ; resolves to @policy.drivers[n].dateOfBirthValidation
Validation constraints live inline with field definitions. They are declared as properties
on the field element and are inherited from @validation.
| Property | Type | Applies To | Description |
|---|---|---|---|
required | boolean | All | Field must have a non-empty value on submit |
pattern | regex string | text | Value must match this regular expression |
minLength | integer ≥ 0 | text | Minimum character count |
maxLength | integer ≥ 1 | text | Maximum character count |
min | number / date | date, number | Minimum value (inclusive) |
max | number / date | date, number | Maximum value (inclusive) |
minSelect | integer | multiselect | Minimum number of selections |
maxSelect | integer | multiselect | Maximum number of selections |
{.field.ssn}
type = "text"
label = "Social Security Number"
required = ?true
pattern = "^\d{3}-\d{2}-\d{4}$"
minLength = ##11
maxLength = ##11
bind = @insured.ssn
{.field.age}
type = "text"
label = "Age"
required = ?true
min = ##18
max = ##120
bind = @insured.age
{.field.dob}
type = "date"
label = "Date of Birth"
min = 1900-01-01
max = 2010-01-01
bind = @insured.birthDateValidation errors are surfaced to the user before submission. The renderer must display
errors adjacent to the offending field, with ARIA attributes set to link the error message
to the field (aria-describedby).
Pages & Templates
Multi-page forms use indexed {page[n]} sections. Pages are 0-indexed. Each
page is independently rendered as a separate page in the PDF or a separate scrollable
viewport on screen. Page defaults from {$.page} apply to all pages.
{page[0]}
; Page 1: Applicant Information
{.text.header}
content = "Personal Auto Insurance Application"
{page[1]}
; Page 2: Vehicle Information
{.text.header}
content = "Vehicle Information — Page {@odin.page} of {@odin.total_pages}"Regions
Regions group repeating content with overflow handling. When bound to an array, child
elements repeat for each item. When items exceed max, overflow pages are
generated automatically.
| Property | Type | Required | Description |
|---|---|---|---|
x, y | number | Yes | Region origin position |
w, h | number | Yes | Region bounding box |
bind | path ref | Yes | Path to array data source |
max | integer | Yes | Maximum items per page before overflow |
overflow | string | No | clone or template reference (e.g., @tpl_name) |
Child elements inside a region use coordinates relative to the region's origin. The y-offset property controls vertical spacing between repeated items.
{.region.vehicles}
x = #0.5
y = #1.2
w = #7.5
h = #6
bind = @policy.vehicles
max = ##3
overflow = @tpl_vehicles_continued
{.region.vehicles.field.vin}
x = #0
y = #0.15
y-offset = #1.8
w = #4
h = #0.3
label = "VIN"
required = ?true
bind = @.vin
{.region.vehicles.field.year}
x = #0
y = #0.7
y-offset = #1.8
w = #1
h = #0.3
label = "Year"
bind = @.yearOverflow Modes
| Mode | Behavior |
|---|---|
clone | Duplicates the entire page. All static elements repeat. The region continues with the next batch of items. |
@tpl_name | Instantiates a page template for continuation pages. Allows different headers, footers, or layouts. |
Page Templates
Page templates define layouts for dynamically generated continuation pages. They are not
rendered directly, they are instantiated when a region overflows. Templates use the {@tpl_*} naming convention.
| Property | Type | Description |
|---|---|---|
page-template | boolean | Marks this section as a template, not a concrete page |
continues | string | Names the region this template continues |
form-id | string | Form identifier for continuation page headers |
{@tpl_drivers_continued}
page-template = ?true
continues = "region.drivers"
form-id = "ACORD 125 (Cont)"
{.text.header}
x = #0.5
y = #0.5
content = "Additional Drivers (Continued) — Page {@odin.page} of {@odin.total_pages}"
font-size = ##14
font-weight = "bold"
{.region.drivers}
x = #0.5
y = #1
w = #7.5
h = #8
max = ##6
overflow = @tpl_drivers_continued ; recursive for unlimited pagesConcrete vs. Template Pages
| Aspect | Concrete Page | Page Template |
|---|---|---|
| Header | {page[n]} | {@tpl_name} |
| Rendered | Always | Only on overflow |
| Counted | Yes, fixed index | Yes, dynamic position |
| Variables | {@odin.page} resolves to index + 1 | {@odin.page} resolves at runtime |
Render-Time Variables
Variables in the @odin namespace are resolved at render time. Use them in
any string property via interpolation syntax: {@odin.variable}.
| Variable | Type | Description |
|---|---|---|
@odin.page | integer | Current page number (1-based) |
@odin.total_pages | integer | Total pages after overflow calculation |
{.text.page_number}
content = "Page {@odin.page} of {@odin.total_pages}"
{.text.footer}
content = "Form PA-100 — {@odin.page}"Interpolation works in any string property: content, label, alt, aria-label. The @odin.total_pages value
requires a two-pass render, renderers must calculate overflow first, then render
with the final page count.
Rendering Targets
ODIN Forms targets two primary rendering environments. Both produce output from the same form definition without modification to the form document.
| Target | Technique | Notes |
|---|---|---|
| HTML/CSS | Absolute positioning, CSS scoping | Each page is a fixed-dimension div. Elements positioned with position: absolute, coordinates converted from document units to pixels at a configurable DPI. |
| Browser print / headless Chrome | Print CSS constrains page breaks to page boundaries. PDF output mirrors screen rendering exactly. |
HTML/CSS Rendering Notes
- Page container:
position: relative, fixed pixel dimensions - Elements:
position: absolute,leftandtopfrom coordinates - CSS classes scoped to form ID to prevent leakage
- Scale applied via CSS
transform: scale()for zoom support - Interactive fields rendered as native HTML form elements
Print CSS
/* Each page is a discrete print page */
@media print {
.odin-page { page-break-after: always; }
.odin-page:last-child { page-break-after: auto; }
}Accessibility
Accessibility is built into ODIN Forms from day one. The spec mandates accessible output from conformant renderers, it is not an optional layer. Both PDF (Tagged PDF) and HTML (ARIA) output must meet WCAG 2.1 Level AA.
HTML Renderer Requirements
- All interactive fields must have ARIA roles appropriate to their type
- Every
inputorselectmust be associated with itslabelviafor/idpairing - Radio groups must be wrapped in
fieldsetwith alegend - Required fields must carry
aria-required="true" - Validation errors must set
aria-invalid="true"andaria-describedbypointing to the error message - Tab order follows the form's field sequence; override with
tabindexonly when necessary - Multi-page forms must provide skip links to navigate between pages
- Color contrast must meet WCAG AA minimum (4.5:1 for normal text, 3:1 for large text)
PDF Renderer Requirements
- Tagged PDF structure is required, form fields emit
/Formtags - Text elements emit appropriate
/P,/H1–/H6tags based on context - Images must include
/Altentries from thealtproperty - Barcodes must include
/Altentries from thealtproperty - Reading order must follow visual tab order
Form Definition Requirements
- Every field must have a non-empty
labelproperty - Every
imgmust have a non-emptyaltproperty - Every
barcodemust have a non-emptyaltproperty - Use
aria-labelto provide a clearer screen reader label when the visible label is abbreviated or ambiguous - Decorative elements (lines, rects used purely for visual structure) should be marked with
aria-hiddenby the renderer
{.field.ssn}
type = "text"
label = "Social Security Number"
aria-label = "Social Security Number — format: 123-45-6789"
placeholder = "123-45-6789"
required = ?true
pattern = "^\d{3}-\d{2}-\d{4}$"
bind = @insured.ssn