HTML attributes
Every HTML attribute is authored with bracket syntax. attr['value'] writes a literal string, a bare attr with no brackets writes a boolean flag, and a value containing [expr] interpolates against scope at render time — the same rule used for text content. There is no allowlist; any attribute name passes through.
Quoted literal
The workhorse form
Brackets hold the attribute value verbatim. Quote it when the value is a string literal; the renderer emits the standard name-equals-value shape.
<a href['/about'] title['About this site']> About Boolean flags
No brackets, no value
Attributes without brackets render as bare flags — the HTML5 shorthand for boolean presence. Same rule for every attr that takes no value.
<input type['email'] required autocomplete['email']>
<input type['checkbox'] checked disabled> snake_case becomes kebab-case
One rule, no per-attr branching
Rat identifiers cannot carry a literal hyphen, so kebab-case HTML attributes are authored with underscores. Every underscore in the attribute name lowers to a hyphen in the rendered HTML — aria_label becomes aria-label, data_user_id becomes data-user-id, accept_charset becomes accept-charset. There is no allowlist of supported attrs; the renderer maps every name through this same path, so anything the WHATWG spec or your custom element accepts will reach the DOM.
<button aria_label['Close dialog'] data_action['close']> ×
<form accept_charset['utf-8']> Interpolated values
Bracket reads inside the quoted string
A bracket inside the attribute value resolves the expression at render time. Reads of page state — anything declared under the page block — also re-emit a reactive-attr config, so a later write re-applies the attribute without a full re-render. See Reactivity for the write side.
> server
slug: 'getting-started'
> page
count: 3
<a href['/posts/[slug]']> Read post
<button data_count['[count]'] on_click[count << count + 1]> [count] clicks Bare identifier inside brackets is a literal string
Quote to read state, not to make it a name
The bracket payload is captured as text — a bare identifier inside the brackets renders as that literal name, not a read of a page var. To pull a state value into an attribute, wrap the read in a quoted string: write value['[hue]'], not value[hue]. This is the same string-interpolation rule that text content uses; the brackets are the value, and any read inside the string happens at render time.
> page
hue: 200
<input value['[hue]'] type['number']> DOM-property attributes
text_content and inner_html lower to JS properties, not HTML attrs
A handful of attribute names map to JavaScript DOM properties instead of HTML attributes. text_content and inner_html both write the element's body, not a name-equals-value pair on the tag — at render time the value flows into the element's children, and on a later state write the runtime assigns element.textContent or element.innerHTML directly. Use text_content when the value is plain text; use inner_html when the value is trusted markup that should pass through raw. Any author-written children are replaced.
> page
greeting: 'hello, world'
markup: '<em>now</em>'
<span text_content['[greeting]']>
<span inner_html['[markup]']> Dual-write attributes
value, checked, selected, open keep both attr and live property in sync
Four attribute names dual-write: value, checked, selected, open. The HTML attribute lands on the markup for view-source and hydration; the runtime also assigns the JS property on every state change, because typing into a focused input or toggling a details element detaches the live property from the attribute. Without the dual-write a state update would be visible to view-source but invisible to the user. Authoring is the same as any other attribute — Rat picks the right write path from the resolved name.
> page
name: 'Ada'
agreed: true
<input value['[name]'] on_input[name << event.target.value]>
<input type['checkbox'] checked['[agreed]'] on_change[agreed << event.target.checked]> Event handlers
on_click and friends — same bracket shape
Event handlers are authored the same way as any other attribute, with the body holding a Rat expression instead of a string. The handler compiler routes calls to page state, services, and page-tier functions through the right dispatch path — see the Reactivity page for the full grammar.
> page
name: ''
<input value['[name]'] on_input[name << event.target.value]>
<button on_click[name << '']> Clear