Template Syntax

Hologram templates use a custom syntax called "HOLO" that combines HTML with Elixir expressions.

Regular HTML Markup

You can use standard HTML elements and attributes in your templates:

<div class="container">
  <h1>Hello World</h1>
  <p>This is a paragraph.</p>
</div>

Accessing Props and State

Props and state are accessible in templates using the @var syntax. This provides a convenient way to reference component data. For example, if you have a prop or state variable named count, you can access it as @count in your template.

In the following sections, you'll see how to use these variables in Elixir expressions for interpolation and control flow blocks.

Component Nodes

Components can be used as custom elements in your templates. They can receive props (properties) as attributes. String props can be given as regular double-quoted attributes, while other Elixir values (like numbers, booleans, or expressions) are given using curly braces syntax:

<MyComponent title="Hello" count={42} />

For more information about components and their props, see the Components documentation.

Dynamic Nodes

Which node a tag renders doesn't have to be decided literally. A braced Elixir expression in tag-name position is evaluated at render time and its value decides what gets rendered: a component module renders that component, a string renders an HTML element with that tag name, and any other value raises an ArgumentError.

Components are the usual reason to reach for this - a widget chosen by a content type, a step in a wizard, a row in a heterogeneous list. The attributes become the component's props and the content between the tags becomes its default slot, exactly as with a statically named component:

<{@module} cid="my_component" title="Hello" />
<{@module} cid="my_component">slot content</{@module}>

A page module isn't valid here, even though a page is a kind of component itself - it's a root one, owning a route, taking its params from the URL and picking a layout, so it only makes sense at the root of a render, never as a node inside another template.

A call site that can't name the module statically usually can't name the props either, so dynamic tags pair naturally with the attribute and prop spread described below - both the module and its props come from the data:

{%for widget <- @widgets}
  <{widget.module} ...{widget.props} />
{/for}

A string value renders an element instead, with the attributes becoming DOM attributes and event bindings - so a heading level or an anchor-versus-button choice can be kept in state. Whatever string arrives is the tag that gets rendered, so tag names must never come from untrusted input:

<{@heading_tag} class="heading">{@text}</{@heading_tag}>

The expression is ordinary Elixir compiled in the template's module context, so aliases in scope apply and @var resolves to a prop or state value, exactly as anywhere else in a template. Most often it's simply a state value holding a module or a tag name:

put_state(component, module: MyButton, heading_tag: "h2")

Nothing beyond the dispatch is new. Once resolved, a dynamic node behaves exactly like the equivalent static one - <{MyButton} label="x"> is <MyButton label="x"> and <{"div"} class="x"> is <div class="x">. All prop, attribute, event, void element, spread and slot semantics carry over unchanged.

Since dispatch happens per render, changing the module under a given cid replaces the component: the outgoing one is unmounted and the incoming one initializes its own state from scratch.

Closing Tags

A dynamic tag with children is closed with </{expr}>, repeating the opening expression verbatim, as in the examples above.

That repetition is a good reason to keep the expression short. Logic written inline - an if picking between an anchor and a button, a case mapping a content type to a component - gets spelled out twice, once in each tag, and the two copies can drift apart. Keep the value in state or a prop, or compute it with a helper function.

Module Reachability

To render a dynamic component on the client, the compiler must know its module at build time. It bundles a component whenever the module atom appears as a literal in code it can see - a template, any client-reachable function, a page's or component's init/3 or command/3 server callback, or any code that broadcasts an action - put_broadcast/3,4 inside a handler, Hologram.Realtime.broadcast_action/2,3 outside one. So a module put into state by init/3 or sent back in action params by command/3 works without any extra declaration.

The search follows calls forward, so functions those callbacks call count too, but their own callers don't - a module picked elsewhere and passed down into a broadcasting helper won't be found. Modules that exist only at runtime don't work at all: ones built with Module.concat/1, resolved with String.to_existing_atom/1, or read from the database or config without a literal anywhere in the app's code are not bundled, and rendering them on the client raises.

A component referenced in broadcast code goes into the shared runtime bundle rather than a page bundle, so it ships with every page - a broadcast is addressed to a cid, and which page has that cid mounted is only known at runtime.

Elixir Expression Interpolation

You can embed Elixir expressions in your templates using curly braces:

Inside Text

<p>Hello, {@name}!</p>

Inside Attributes and Props

You can interpolate Elixir expressions in attributes and props in two ways.

First, you can interpolate the entire attribute value:

<div class={@class_name}>Content</div>

Or you can interpolate part of the attribute value within double quotes:

<div class="base-class {@dynamic_class}">Content</div>

Both approaches work for regular HTML attributes and component props:

<MyComponent count={Enum.count(@items)} label="Welcome, {@count}" />

Conditional Attributes

When an attribute expression evaluates to a falsy value (nil or false), the attribute is not rendered at all. This is useful for conditional attributes:

<button disabled={@loading?}>Submit</button>
<div class={if @active? do "active" else nil end}>Content</div>

In this example, when @loading? is false, the disabled attribute won't appear in the HTML. Similarly, when @active? is false, the class attribute will be omitted entirely.

Security: Automatic HTML Escaping

For security purposes, all interpolated expressions are automatically HTML-escaped to prevent XSS (Cross-Site Scripting) attacks. This means that potentially dangerous characters like <, >, &, and quotes are converted to their HTML entity equivalents (&lt;, &gt;, &amp;, etc.).

<p>User input: {@user_input}</p>

If @user_input contains "<script>alert('XSS')</script>", it will be safely rendered as escaped text rather than executed as JavaScript. This escaping happens automatically for all values interpolated in text content and HTML attributes.

Attribute and Prop Spread

A bare braced expression prefixed with ... injects a map's or keyword list's entries as the tag's attributes (on elements) or props (on components). The ... marker sits outside the braces, so their content is ordinary Elixir - any expression is allowed:

<div class="btn" ...{@html_attrs}>Content</div>
<MyComponent title="Hello" ...{@props} />

The expression must evaluate to a map or a keyword list - anything else, including nil, raises an ArgumentError. Bare keyword shorthand works inside the braces, the same convention event bindings already use:

<div ...{class: "btn", data: [user_id: @id]}>Content</div>

Spread adds no rules of its own: every entry behaves exactly as if you had written it as a named attribute or prop at that position. On elements, values are stringified and falsy ones drop the attribute, exactly as described above. On components, values stay raw Elixir terms and undeclared keys are silently ignored, just like named props.

Attribute Names

On elements, underscores in a key convert to hyphens, whether the key is an atom or a string - :data_value and "data_value" both render data-value. A map or keyword list value nests, composing the name with hyphens at each level, so the example above renders data-user-id. Literal markup is never touched, so writing the attribute out by hand remains the exact-name escape hatch.

On components, neither rule applies. A key like :my_prop sets the prop declared with exactly that name, with no hyphen conversion, since prop names are Elixir atoms. Values don't nest either - a map value is not expanded into multiple props, it's simply what that one prop receives. The cid prop may be supplied through a spread.

Event bindings cannot be set through a spread. A $-prefixed key raises an ArgumentError, because silently not binding an intended event would be worse than erroring. Write events as literal attributes. They don't need a spread to be conditional anyway - a binding whose operation resolves to nil is switched off, as described in the Events documentation.

The entries a single spread expands to render in alphabetical order. Map key order is undefined in Elixir, so sorting is what keeps the output reproducible. Attributes you write out yourself keep their position.

Precedence

Multiple spreads are allowed, and they interleave freely with named attributes and props. Names resolve by position, and the last one wins - so a literal before a spread is a default, and a literal after it is forced:

<div title="default" ...{@attrs}>Content</div>
<div ...{@attrs} title="forced">Content</div>
<div ...{@base} ...{@overrides}>Content</div>

Forwarding Wrapper

There is nothing to declare on the receiving component - spread resolves at the call site, so props arrive indistinguishable from named ones and every existing component is already a valid target. To let a wrapper pass arbitrary attributes through to an inner element, declare an ordinary map prop:

prop :html_attrs, :map

and spread it in the wrapper's template:

<button class="fancy" ...{@html_attrs}><slot /></button>

Callers then pass the attributes as a single prop:

<FancyButton html_attrs={@button_attrs}>Save</FancyButton>

Control Flow Blocks

If Block

Use {%if} blocks for conditional rendering. The condition follows Elixir's truthiness rules - only nil and false are considered falsy, while any other value is considered truthy. You can optionally include an {%else} branch.

Simple if block without else:

<div>
  {%if @show_message?}
    <p>Message is visible</p>
  {/if}
</div>

If block with else branch:

<div>
  {%if @show_message?}
    <p>Message is visible</p>
  {%else}
    <p>Message is hidden</p>
  {/if}
</div>

For Block

Use {%for} blocks to iterate over collections. The syntax follows Elixir's comprehension rules, allowing you to use the same pattern matching and filtering capabilities as regular Elixir comprehensions.

<ul>
  {%for item <- @items}
    <li>{item.name}</li>
  {/for}
</ul>

Event Binding

You can bind events to elements using event attributes. For detailed information about event binding syntax and available event types, see the Events documentation.

Escaping Curly Braces

To output literal curly braces in your template, escape them with a backslash:

<p>\{@literal\} {@variable}</p>

Raw Block

Use {%raw} blocks to output content without processing:

<div>
  {%raw}
    This content will be output as-is, including any \{curly braces\} or \{%control\}...\{/flow\} syntax.
  {/raw}
</div>

HTML Comments

You can use standard HTML comments in your templates:

<div>
  <!-- This is a comment that will be visible in the HTML output -->
  <p>Content</p>
</div>
Sponsored by
Curiosum
Main sponsor
Erlang Ecosystem Foundation
Milestone sponsor