Template Syntax
LUAT uses a Svelte-inspired syntax for expressing dynamic content, control flow, and component composition.
Expressions
Text Interpolation
Use curly braces to output dynamic content. Values are automatically HTML-escaped for security:
<h1>{props.title}</h1>
<p>Welcome, {props.user.name}!</p>
<span>Price: ${props.price}</span>
Raw HTML
Use {@html}
to output unescaped HTML content:
<div>{@html props.richContent}</div>
⚠️ Warning: Only use {@html}
with trusted content to avoid XSS vulnerabilities.
Attributes
Dynamic attributes work the same as text interpolation:
<!-- Dynamic attributes -->
<img src={props.imageUrl} alt={props.imageAlt}>
<div class="card {props.extraClass}">
<button disabled={props.isLoading}>Submit</button>
Attribute Shorthand
When an attribute name is the same as the variable holding its value, you can use a shorthand.
<script>
local title = "A descriptive title for the link"
</script>
<!-- This shorthand... -->
<a href="/some-page" {title}>A link</a>
<!-- ...is equivalent to this: -->
<a href="/some-page" title={title}>A link</a>
For more advanced techniques, including conditional classes and integrating with client-side libraries, see the Dynamic Attributes & Classes guide.
Control Flow
If Blocks
Conditional rendering with {#if}
, {:else}
, and {/if}
:
{#if props.user.isAdmin}
<div class="admin-panel">
<h2>Admin Dashboard</h2>
</div>
{:else}
<div class="user-panel">
<h2>User Dashboard</h2>
</div>
{/if}
Multiple conditions with {:else if}
:
{#if props.status === "loading"}
<div class="spinner">Loading...</div>
{:else if props.status === "error"}
<div class="error">Something went wrong</div>
{:else}
<div class="content">{props.data}</div>
{/if}
Each Blocks
Iterate over arrays with {#each}
, {:empty}
, and {/each}
:
<ul>
{#each props.items as item}
<li>{item.name}</li>
{/each}
</ul>
With index:
<ol>
{#each props.items as item, index}
<li>{index + 1}. {item.name}</li>
{/each}
</ol>
With empty state:
<div class="item-list">
{#each props.items as item}
<div class="item">{item.name}</div>
{:empty}
<div class="no-items">No items found</div>
{/each}
</div>
Props Spread Operator
The spread operator (...
) allows you to pass all properties from a table to a component. This is useful when you want to pass multiple properties at once without listing each one individually.
<script>
local Button = require("components/Button")
local buttonProps = { label = "Save", type = "button" }
</script>
<Button {...buttonProps} />
In this example, all key-value pairs from buttonProps
will be passed to the Button
component.
Combining Multiple Spreads
You can use multiple spread operators in a single component:
<script>
local Card = require("components/Card")
local baseProps = { title = "My Card", size = "medium" }
local themeProps = { variant = "primary", outlined = true }
</script>
<Card {...baseProps} {...themeProps} />
Combining Spreads with Regular Props
Spread operators can be used together with regular prop assignments. Properties defined directly will override those from spread objects:
<script>
local Button = require("components/Button")
local baseProps = { label = "Default", type = "button" }
</script>
<Button {...baseProps} label="Submit" />
In this case, the button will render with label="Submit"
(from the direct prop) and type="button"
(from the spread).
Precedence Rules
When multiple properties with the same name are specified, the precedence is as follows:
- Direct props (highest precedence)
- Spread operators, with later spreads overriding earlier ones
<script>
local baseProps = { type = "button", size = "medium" }
local overrideProps = { size = "large" }
</script>
<!-- Results in type="reset" (direct prop), size="large" (from overrideProps) -->
<Button {...baseProps} type="reset" {...overrideProps} />
Declaring Local Variables
The {@local}
tag allows you to declare local variables directly within a block's scope. This is useful for creating derived values that you want to reference multiple times, making your templates cleaner and more efficient without needing a separate <script>
block.
The variable is only available inside the block where it is defined.
{#each products as product}
{@local availability = getAvailability(product.stock)}
{@local finalPrice = product.price * (1 - product.discount)}
<div class="product-card">
<h3>{product.name}</h3>
<p>Price: ${finalPrice}</p>
<p class="{availability.class}">{availability.text}</p>
</div>
{/each}
{@local}
is particularly powerful inside control flow blocks like {#if}
and {#each}
, and also within components.
{#if user.sessionValid}
{@local displayName = user.name or "Anonymous"}
<p>Welcome back, {displayName}!</p>
{/if}
Usage Constraints
The {@local}
tag must be an immediate child of a block or component. It cannot be used at the top level of a template.
<!-- Invalid: {@local} cannot be a top-level tag -->
{@local myVar = 'this will cause an error'}
Sensitive Content
For logging and debugging purposes, mark sensitive content blocks that should be tracked:
Sensitive If Blocks
{!if props.showSecret}
<!-- This block will be logged as sensitive -->
<div class="secret">
<p>API Key: {props.apiKey}</p>
</div>
{/if}
Sensitive Each Blocks
{!each props.secrets as secret}
<div class="secret-item">{secret.value}</div>
{/each}
These blocks function identically to regular control flow but are marked for special handling in logs and debugging tools.
Comments
HTML Comments
Standard HTML comments are preserved in the output:
<!-- This comment appears in the rendered HTML -->
<div>Content</div>
Template Comments
LUAT comments are removed during compilation:
{/* This comment is only visible in the source */}
<div>Content</div>
Whitespace Handling
LUAT preserves whitespace in your templates, but you can control it:
Compact Syntax
<span>{props.firstName}</span> <span>{props.lastName}</span>
Multi-line with Preserved Formatting
<div class="poem">
{props.line1}
{props.line2}
{props.line3}
</div>
Advanced Expressions
Lua Expressions
Any valid Lua expression can be used within braces:
<!-- Arithmetic -->
<span>Total: {props.price * props.quantity}</span>
<!-- String operations -->
<h1>{string.upper(props.title)}</h1>
<!-- Conditional expressions -->
<div class="{props.isActive and 'active' or 'inactive'}">
<!-- Function calls -->
<p>Formatted date: {formatDate(props.createdAt)}</p>
Object Access
Access nested properties with dot notation:
<div>
<h2>{props.user.profile.displayName}</h2>
<img src={props.user.profile.avatar.url}>
<p>{props.user.settings.theme}</p>
</div>
Array Access
<div>
<h3>First item: {props.items[1].name}</h3>
<p>Item count: {#props.items}</p>
</div>
Special Characters
Escape special characters when needed:
<!-- Literal braces -->
<pre>Use \{curly braces\} for expressions</pre>
<!-- Quotes in attributes -->
<button onclick="alert('Hello {props.name}')">Click me</button>
Best Practices
1. Keep expressions simple
<!-- Good -->
<span>{props.price}</span>
<!-- Better extracted to script -->
<script>
local formattedPrice = formatCurrency(props.price)
</script>
<span>{formattedPrice}</span>
2. Use meaningful variable names
<script>
local userName = props.user and props.user.name or "Guest"
local isLoggedIn = props.user ~= nil
</script>
<div class="header">
{#if isLoggedIn}
<span>Welcome, {userName}!</span>
{/if}
</div>
3. Handle nil/undefined gracefully
<!-- Safe property access -->
<img src={props.user and props.user.avatar or "/default-avatar.png"}>
<!-- With helper functions -->
<script>
local avatarUrl = getAvatarUrl(props.user)
</script>
<img src={avatarUrl}>