RatMinimal web framework

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

Boolean flags

No brackets, no value

Attributes without brackets render as bare flags, the HTML5 shorthand for boolean presence. The same rule applies to every attr that takes no value.

<input type['email'] required autocomplete['email']>
<input type['checkbox'] checked disabled>
Result

Kebab-case names: hyphens or underscores

Both spellings reach the DOM hyphenated

Kebab-case HTML attributes work two ways. A literal hyphen in the name passes through verbatim: data-user-id7 ships exactly as written, in standalone tags and inline in prose. Underscores also lower to hyphens, so aria_label becomes aria-label and accept_charset becomes accept-charset. That lets you author the same attribute in Rat's identifier style. 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']>
Result

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
Result

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']>
Result

DOM-property attributes

text_content and inner_html lower to JS properties, not HTML attrs

A few 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. 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.

Security: inner_html writes raw markup - it is the one place Rat does not escape for you, exactly like React's dangerouslySetInnerHTML. Only ever hand it markup you authored. Never feed it a value that can carry request input, database rows, or anything a user typed: a stored <script> (or an onerror attribute) runs as code in every visitor's browser. When the value is anything but your own literal markup, use text_content - it escapes.

> page
greeting: 'hello, world'
markup: '<em>now</em>'

<span text_content['[greeting]']>
<span inner_html['[markup]']>
Result
hello, worldnow

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]>
Result

Event handlers

on_click and friends use the 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
Result

See also Reactivity · Interpolation · Inline styles