Displaying Data
Introduction
Ree has three output tags plus three translation-lookup tags. Two of the output tags print values into the rendered HTML - one escapes its input, the other doesn't; the third runs plain JavaScript inline. The translation-lookup tags read strings from props.translations with a built-in safety net for missing keys - one escapes, one emits raw HTML, and one renders the value through markdown. Most templates use the escaped output tag for data and the escaped translation tag for copy, reaching for the others only when needed.
Escaped Output
{= expr } outputs an expression with HTML escaping. The characters &, <, >, ", and ' are converted to their entity equivalents before being inserted into the page, so user-supplied content cannot inject markup or break out of an attribute:
<h1>{= props.title }</h1>
<p>Welcome back, {= props.user.name }.</p>
This is the safe default. If you're not sure which output tag to use, use this one. Even values that you know come from your own database are best escaped - a column might one day hold something a user typed in.
The expression inside the tag is plain JavaScript. You can call methods, access nested properties, and use ternaries:
<p>{= props.user.name?.toUpperCase() ?? "Anonymous" }</p>
<span class="badge {= props.is_admin ? 'badge-admin' : 'badge-user' }"> {= props.tags.join(", ") } </span>
Raw HTML Output
{~ expr } outputs an expression without escaping. Use it when the value already contains trusted HTML - a markdown-rendered string, a sanitised rich-text field, a snippet you composed in your handler:
{~ props.rendered_markdown }
Never use {~ } with values that came from user input. The whole point of the escaped tag is to defend against injection; the raw tag turns that defence off. If your handler computed an HTML string from a value the user submitted, escape it before passing it in (or pre-escape parts of it and concatenate trusted markup around them).
Translation Lookups
{_ path }, {- path } and {@ path } read a translation string from props.translations - the merged translations for the current page and language, loaded from your {lang}.json files. {_ } escapes its output (like {= }); {- } outputs raw HTML (like {~ }, for the rare translation value that legitimately contains markup); {@ } renders the value through markdown to HTML:
<a href="{~ localized_path('/') }">{_ nav.home }</a>
<h1>{_ ui.title }</h1>
path is a plain dotted key path - nav.home, ui.title, blog.read_more. It is not arbitrary JavaScript: no function calls, no computed keys (nav[key]), no expressions. That restriction lets the engine resolve the value with a safe property walk instead of evaluating it, so a missing key can never crash the render.
Missing keys render a marker, never a blank or a crash. If the key isn't present, the tag renders the last segment wrapped in braces - a missing {_ nav.home } renders as {home}. Combined with the cross-language fallback, which fills a missing key from another language first, an untranslated string shows up loud on the page rather than disappearing.
Use these for every read from props.translations. {= } / {~ } can still reach translation data directly ({= props.translations.ui.title }), but that bypasses the missing-key marker. Reserve {= } / {~ } for everything that is not a translation: page data, loop variables, computed expressions.
Markdown translations - {@ path }
{@ path } resolves the translation exactly like {_ } / {- }, then renders the value through markdown to HTML (via Bun.markdown.html). Use it when a translation string is authored as markdown source - headings, lists, **bold**, links - so the content in your {lang}.json files stays plain markdown instead of hand-written HTML:
<article>{@ pages.about_body }</article>
With pages.about_body set to "## Our story\n\nWe build **fast** tools.", this renders an <h2> and a paragraph with a <strong>. An empty or absent value renders nothing; a missing key renders the {last_segment} marker (which markdown wraps in a <p>). {@ } emits block-level HTML, so unlike {_ } / {- } it is a body-only tag - it can't be used in an attribute value.
Inline JavaScript
{{ ... }} runs arbitrary JavaScript at render time. The block produces no output of its own - its job is to compute values that you reference later in the template:
{{
const label = props.record.id ? props.labels.edit : props.labels.new;
const class_map = { red: "bg-red-600 text-white", green: "bg-green-600 text-white" };
const banner_class = class_map[props.type] ?? "border border-neutral-300";
}}
<h1>{= label }</h1>
<div class="{~ banner_class }">{~ props.text }</div>
Variables declared inside {{ ... }} are available for the rest of the template. They are scoped to the same lexical block the rest of your template runs in, so you can use const and let freely.
The most common patterns:
-
Pre-sorting or filtering a list before iterating:
{{ const recent = props.posts.slice(0, 5) }} {#each recent as post } <article>{= post.title }</article> {/each} -
Building a class string from a small lookup table:
{{ const color = { admin: "red", staff: "blue", user: "gray" }[props.role] }} <span class="badge badge-{= color }">{= props.role }</span> -
Pulling a frequently-used value out into a short alias:
{{ const u = props.user }} <p>{= u.first_name } {= u.last_name } ({= u.email })</p>
Keep these blocks small. If a template starts to fill up with {{ ... }} blocks, the work usually belongs in the handler instead - pass the precomputed value into props and reference it directly.
Comments
HTML comments are stripped during compilation. Anything inside <!-- ... --> is removed before rendering, and template tags inside a comment are never evaluated. This makes commenting-out a block safe (a {= props.missing } inside a comment won't error), but comments never reach the rendered HTML.
Escaping Rules in Detail
The escape function is single-pass and converts exactly five characters:
| Character | Replacement |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
null and undefined are converted to the empty string. Numbers, booleans, and dates are coerced with String(value) first, then escaped.
The same rules apply whether the value lives in element content (<p>{= x }</p>), in an attribute (<input value="{= x }">), or in a URL (<a href="{= x }">). For attributes specifically, always wrap the value in double quotes - that combined with " being escaped means the value cannot break out of the attribute.