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

  1. Print-first: Fixed page dimensions, absolute coordinates
  2. Self-describing: Element types in path structure
  3. HTML5/SVG naming: Familiar attribute names reduce learning curve
  4. Bidirectional binding: Forms bind to ODIN data documents
  5. Validation inline: Field constraints live with field definitions
  6. Composable: Reuse common element definitions
  7. 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:

SectionKeyRequiredDescription
Metadata{$}YesDocument-level settings: version, id, title, lang
Localization{$.i18n}NoMulti-language labels by language code
Page defaults{$.page}YesDefault page dimensions, unit, and margins
Screen settings{$.screen}NoScreen rendering options
Pages{page[n]}YesArray of page definitions with elements
Complete document skeleton
{$}
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 elements

Metadata

The {$} section declares document identity and version. These fields are required in every ODIN Forms document.

PropertyTypeDescription
odinversionODIN specification version (1.0.0)
formsversionODIN Forms schema version (1.0.0)
titlestringHuman-readable form title
idstringUnique form identifier (reverse-domain or slug)
langstringDefault language code (e.g., en, es)
versionstringForm 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 labels
{$.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_name

Page Defaults

The {$.page} section sets dimensions that apply to all pages. Every page inherits these values unless overridden.

PropertyTypeDescription
widthnumberPage width in declared unit
heightnumberPage height in declared unit
unitenumUnit of measure: inch, cm, mm, pt
margin.topnumberTop margin
margin.rightnumberRight margin
margin.bottomnumberBottom margin
margin.leftnumberLeft margin
Page defaults
{$.page}
width = #8.5
height = #11
unit = "inch"
margin.top = #0.5
margin.right = #0.5
margin.bottom = #0.5
margin.left = #0.5

Screen Settings

Optional settings that control how the form renders on screen. These have no effect on print or PDF output.

PropertyTypeDescription
$.screen.scalenumberDefault 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.

PropertyDescription
OriginTop-left of page
X-axisLeft to right (increasing X moves right)
Y-axisTop to bottom (increasing Y moves down)
UnitDeclared in $.page.unit

Supported Units

UnitIdentifierNotes
InchinchStandard US paper measurement
CentimetercmMetric paper measurement
MillimetermmMetric, fine-grained positioning
Pointpt72 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
{@position}
x = !#
y = !#

@dimensions

Width and height for rectangular elements.

@dimensions
{@dimensions}
w = !#
h = !#

@stroke

Stroke properties for geometric elements. Uses SVG-compatible attribute names.

@stroke
{@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}
fill = :/^#[0-9A-Fa-f]{6}$|^none$/
fill-opacity = #:(0..1)

@font

Typography properties for text elements.

@font
{@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
{@validation}
required = ?
pattern =
minLength = ##:(0..)
maxLength = ##:(1..)
min =
max =

@binding

Data binding for fields. The bind property is required on all interactive fields.

@binding
{@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.

PropertyTypeRequiredDescription
x1numberYesStart X coordinate
y1numberYesStart Y coordinate
x2numberYesEnd X coordinate
y2numberYesEnd Y coordinate
strokehex colorNoLine color
stroke-widthnumberNoLine thickness
line example
{page[0]}
{.line.header_rule}
x1 = #0.5
y1 = #1
x2 = #8
y2 = #1
stroke = "#000000"
stroke-width = #0.5

rect

A rectangle. Inherits @position, @dimensions, @stroke, and @fill.

PropertyTypeRequiredDescription
x, ynumberYesTop-left corner position
w, hnumberYesWidth and height
rxnumberNoHorizontal corner radius
rynumberNoVertical corner radius
fillhex / noneNoFill color
strokehex colorNoBorder color
rect example
{.rect.section_box}
x = #0.5
y = #2
w = #7.5
h = #3
fill = "#f5f5f5"
stroke = "#cccccc"
rx = #0.1
ry = #0.1

circle

A circle defined by center point and radius.

PropertyTypeRequiredDescription
cxnumberYesCenter X coordinate
cynumberYesCenter Y coordinate
rnumber ≥ 0YesRadius

ellipse

An ellipse defined by center point and two radii.

PropertyTypeRequiredDescription
cxnumberYesCenter X coordinate
cynumberYesCenter Y coordinate
rxnumber ≥ 0YesHorizontal radius
rynumber ≥ 0YesVertical radius

polygon

A closed shape defined by a series of points. Points format: x1,y1 x2,y2 x3,y3 ...

polygon example
{.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 example
{.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.

PropertyTypeRequiredDescription
x, ynumberYesPosition (top-left of text baseline)
contentstringYesText to display. Supports {@odin.*} interpolation.
wnumberNoWrap width
hnumberNoClip height
rotatenumberNoRotation in degrees
text example
{.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.

PropertyTypeRequiredDescription
x, ynumberYesPosition
w, hnumberYesDimensions
srcbase64 refYesBase64 image with format prefix (e.g., ^png:...)
altstringYesAccessibility description
backgroundbooleanNoWhen true, renders behind all other elements (lowest z-index). Non-interactive in editing tools.
img example
{.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 = ?true

barcode

1D or 2D barcode. The alt field is required for accessibility, it describes the barcode's purpose for screen readers.

PropertyTypeRequiredDescription
x, ynumberYesPosition
w, hnumberYesDimensions
typeenumYescode39, code128, qr, datamatrix, pdf417
contentstringYesData to encode
altstringYesAccessibility description
barcode example
{.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

PropertyTypeRequiredDescription
typeenumYesField type: text, checkbox, radio, select, multiselect, date, signature
labelstringYesVisible label and accessibility label
x, ynumberYesPosition
w, hnumberYesDimensions
bindpath refYesODIN path for the field value
requiredbooleanNoField must have a value on submit
aria-labelstringNoOverride screen reader text when different from visible label
tabindexinteger ≥ 0NoTab order override
readonlybooleanNoDisplay-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 TypeValue PropertyTypeDescription
textvaluestringCurrent text content
checkboxcheckedbooleanWhether the checkbox is checked
radiovaluestringOption value (the selected radio's value is the group's current value)
selectselectedstringCurrently selected option value
multiselectselectedstring arrayCurrently selected option values
datevaluedateCurrent date value
signaturevaluebinaryCaptured signature data

When a separate bound data document is also present, inline values take precedence.

text

Single-line or multiline text input.

PropertyTypeDescription
valuestringCurrent field value
inputTypeenumScreen 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.
placeholderstringHint text when field is empty
maskstringInput mask pattern (e.g., ###-##-####)
multilinebooleanAllow multiple lines of text
maxLinesintegerMaximum lines when multiline is true
text field example
{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.ssn

checkbox

Boolean checkbox. Binds to a boolean path, true when checked, false or absent when unchecked.

checkbox example
{.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.termsAccepted

radio

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.

PropertyTypeRequiredDescription
groupstringYesRadio group name, same for all options in a set
valuestringYesValue written to bind path when this radio is selected
radio group example
{.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.coverageLevel

select

Single-selection dropdown.

PropertyTypeRequiredDescription
optionsstring arrayYesArray of valid option values
selectedstringNoCurrently selected option value
placeholderstringNoDefault unselected text
select example
{.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 states

multiselect

Multi-selection list. Binds to an array path, the selected values are written as an array.

PropertyTypeRequiredDescription
optionsstring arrayYesArray of valid option values
selectedstring arrayNoCurrently selected option values
minSelectintegerNoMinimum number of selections required
maxSelectintegerNoMaximum number of selections allowed
multiselect example
{.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).

date example
{.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.birthDate

signature

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.

PropertyTypeDescription
date_fieldfield referenceAssociates a date field with this signature for timestamp capture
signature example
{.field.applicant_signature}
type = "signature"
x = #1
y = #9.5
w = #3
h = #0.75
label = "Applicant Signature"
required = ?true
bind = @signatures.applicant

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

Binding examples
{.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 path

Submission Flow

When a form is submitted, the renderer executes this sequence:

  1. Walk all fields in tab order
  2. Validate each field against its constraints (required, pattern, min, max, etc.)
  3. Halt and surface errors if validation fails
  4. 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 relative binding
{.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].dateOfBirth

Validation

Validation constraints live inline with field definitions. They are declared as properties on the field element and are inherited from @validation.

PropertyTypeApplies ToDescription
requiredbooleanAllField must have a non-empty value on submit
patternregex stringtextValue must match this regular expression
minLengthinteger ≥ 0textMinimum character count
maxLengthinteger ≥ 1textMaximum character count
minnumber / datedate, numberMinimum value (inclusive)
maxnumber / datedate, numberMaximum value (inclusive)
minSelectintegermultiselectMinimum number of selections
maxSelectintegermultiselectMaximum number of selections
Validation examples
{.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.birthDate

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

Multi-page form
{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.

PropertyTypeRequiredDescription
x, ynumberYesRegion origin position
w, hnumberYesRegion bounding box
bindpath refYesPath to array data source
maxintegerYesMaximum items per page before overflow
overflowstringNoclone 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 with overflow
{.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 = @.year

Overflow Modes

ModeBehavior
cloneDuplicates the entire page. All static elements repeat. The region continues with the next batch of items.
@tpl_nameInstantiates 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.

PropertyTypeDescription
page-templatebooleanMarks this section as a template, not a concrete page
continuesstringNames the region this template continues
form-idstringForm identifier for continuation page headers
Page template
{@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 pages

Concrete vs. Template Pages

AspectConcrete PagePage Template
Header{page[n]}{@tpl_name}
RenderedAlwaysOnly on overflow
CountedYes, fixed indexYes, 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}.

VariableTypeDescription
@odin.pageintegerCurrent page number (1-based)
@odin.total_pagesintegerTotal pages after overflow calculation
Render-time variable usage
{.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.

TargetTechniqueNotes
HTML/CSSAbsolute positioning, CSS scopingEach page is a fixed-dimension div. Elements positioned with position: absolute, coordinates converted from document units to pixels at a configurable DPI.
PDFBrowser print / headless ChromePrint 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, left and top from 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

Print CSS approach
/* 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 input or select must be associated with its label via for/id pairing
  • Radio groups must be wrapped in fieldset with a legend
  • Required fields must carry aria-required="true"
  • Validation errors must set aria-invalid="true" and aria-describedby pointing to the error message
  • Tab order follows the form's field sequence; override with tabindex only 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 /Form tags
  • Text elements emit appropriate /P, /H1/H6 tags based on context
  • Images must include /Alt entries from the alt property
  • Barcodes must include /Alt entries from the alt property
  • Reading order must follow visual tab order

Form Definition Requirements

  • Every field must have a non-empty label property
  • Every img must have a non-empty alt property
  • Every barcode must have a non-empty alt property
  • Use aria-label to 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-hidden by the renderer
Accessible field definition
{.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