Automatic, framework-level ARIA support, keyboard navigation, and preference adaptation derived from slot metadata annotations and view classes.
STRVCT provides built-in accessibility support that works automatically for every application built on the framework. Because the UI is derived from model nodes through a small set of view classes, accessibility metadata is emitted at the framework level -- developers don't need to add ARIA attributes to individual components.
This is a direct consequence of the naked objects architecture. In a conventional web framework, accessibility is a per-component concern: every button, list, and form field needs manual ARIA attributes, and the cost scales linearly ($O(N)$) with the number of components. In STRVCT, the same $\sim$15 view classes render every application's UI, so fixing accessibility in those classes fixes it everywhere.
Most frameworks require developers to manually add accessibility attributes to each component. STRVCT's architecture inverts this: since the framework generates views from the model, it can also generate accessibility metadata from the model -- automatically, in one place, for all applications.
description() $\rightarrow$ aria-description, isReadOnly() $\rightarrow$ aria-readonly, isRequired() $\rightarrow$ aria-required).setMinimum(0) and setMaximum(100) emits both JSON Schema constraints for AI and aria-valuemin="0" / aria-valuemax="100" for screen readers. The investment in richer slot annotations directly improves both AI integration and accessibility.SvTilesView $\rightarrow$ list, SvTile $\rightarrow$ link, SvFieldTile $\rightarrow$ group, selected tile $\rightarrow$ aria-current).Each core view class emits an appropriate ARIA role:
| View Class | Role | Purpose |
|---|---|---|
SvTilesView | list | Container for navigable tile items |
SvTile | link | Individual navigation item in a list |
SvFieldTile | group | Key-value field with label and value |
SvBooleanFieldTile | checkbox | Toggle field with checked/unchecked state |
SvActionFieldTile | button | Clickable action |
SvOptionsNodeTile | listbox | Container for selectable options |
SvOptionNodeTile | option | Individual selectable option |
SvStackView | navigation | Top-level navigation landmark |
SvNavView | region | Named content region |
SvBreadCrumbsTile | navigation | Breadcrumb navigation with aria-label="Breadcrumb" |
View classes derive ARIA labels from node metadata:
| Source | ARIA Usage | Rationale |
|---|---|---|
node.nodeAriaLabel() | aria-label | title() by default; domain objects can override to provide a descriptive label for screen readers |
node.subtitle() | aria-description | Subtitle text available on demand |
slot.description() | aria-description | Detailed field-level description |
| Key view text | aria-label | Label on field tiles |
SvTitledNode provides methods that domain objects can override to customize accessibility behavior without modifying view code:
| Method | Default | Purpose |
|---|---|---|
nodeAriaLabel() | title() | Label announced by screen readers |
nodeAriaRole() | null (view's default) | Override the view class's default ARIA role |
nodeAriaIsReadOnly() | null (from slot editability) | Override read-only state |
nodeAriaIsRequired() | null (from slot isRequired()) | Override required state |
nodeAriaLive() | null | Returns "polite" or "assertive" for dynamic live regions |
Returning null defers to the view's default behavior. Returning a value overrides it. This follows the same pattern as setNodeViewClass() -- the framework provides sensible defaults, and the domain object overrides when it knows better.
ARIA states are updated dynamically through the framework's notification system:
aria-current -- Set to "true" on the selected tile, removed when deselected. The selected tile also receives focus.aria-checked -- Updated on SvBooleanFieldTile when the boolean value changes.aria-selected -- Updated on SvOptionNodeTile based on isPicked() state.aria-disabled -- Set on SvActionFieldTile when the action's node is not enabled.aria-readonly -- Set on SvFieldTile when the field is not editable.SvFieldTile includes a syncAriaFromSlotMetadata() method that reads slot annotations and emits corresponding ARIA attributes:
{
const slot = this.newSlot("hitPoints", 0);
slot.setSlotType("Number");
slot.setIsRequired(true);
slot.setAnnotation("minimum", 0);
slot.setAnnotation("maximum", 999);
slot.setDescription("Current character hit points");
}Produces a field tile with aria-required="true", aria-valuemin="0", aria-valuemax="999", and aria-description="Current character hit points" -- with no accessibility-specific code from the developer.
nodeAriaLive() returning "polite" or "assertive". When SvTilesView syncs from a node with this method, it sets aria-live on the container.SvChatMessageTile sets aria-live="polite" directly, so new chat messages and AI responses are announced to screen readers as they arrive.tabindex="-1". This makes them reachable by the framework's focus management without adding them to the Tab order.:focus-visible styling (2px solid cornflower blue outline) that appears only for keyboard navigation, not mouse clicks.prefers-contrast: more) increases the focus outline to 3px solid white for maximum visibility.aria-current="true" and DOM focus, ensuring screen readers announce the newly selected item.prefers-reduced-motion: reduce): All animations and transitions are reduced to 0.01ms duration, iteration counts set to 1, and scroll behavior set to auto (instant).prefers-contrast: more): Focus indicators enhanced to 3px solid white with tighter offset.Landmark roles provide structural navigation for screen readers:
SvStackView is marked as navigation -- the top-level navigation structureSvNavView is marked as region with an aria-label derived from the current node's titleSvBreadCrumbsTile is marked as navigation with aria-label="Breadcrumb"| WCAG Criterion | How Addressed |
|---|---|
| 1.3.1 Info & Relationships | ARIA roles and landmarks convey structure |
| 1.3.2 Meaningful Sequence | DOM order matches visual order (flexbox layout) |
| 2.1.1 Keyboard | Arrow keys, Enter, Escape for full navigation |
| 2.3.3 Animation from Interactions | prefers-reduced-motion media query |
| 2.4.1 Bypass Blocks | Landmark roles enable skip navigation |
| 2.4.6 Headings & Labels | aria-label from node title() |
| 2.4.7 Focus Visible | :focus-visible outline styling |
| 3.2.1 On Focus | No context changes on focus |
| 4.1.2 Name, Role, Value | ARIA roles, labels, and states on all interactive elements |
| File | Responsibilities |
|---|---|
_css.css | :focus-visible styling, prefers-reduced-motion, prefers-contrast |
SvTilesView.js | role="list", aria-label, opt-in aria-live |
SvTile.js | role="link", tabindex="-1", aria-label, aria-description, aria-current |
SvFieldTile.js | role="group", aria-label, aria-readonly, syncAriaFromSlotMetadata() |
SvBooleanFieldTile.js | role="checkbox", aria-checked |
SvActionFieldTile.js | role="button", aria-label, aria-disabled |
SvOptionsNodeTile.js | role="listbox" |
SvOptionNodeTile.js | role="option", aria-selected |
SvChatMessageTile.js | aria-live="polite" |
SvStackView.js | role="navigation" |
SvNavView.js | role="region", aria-label |
SvBreadCrumbsTile.js | role="navigation", aria-label="Breadcrumb", aria-current="location" |