Components
Components are the building blocks of LUAT applications. They encapsulate reusable UI elements and can accept props and render children.
Basic Components
Creating a Component
A component is simply a .luat file that exports a render function:
<!-- Button.luat -->
<script>
local variant = props.variant or "primary"
local size = props.size or "medium"
local classes = "btn btn-" .. variant .. " btn-" .. size
</script>
<button class={classes} type={props.type or "button"}>
{@render props.children?.()}
</button>
Using Components
Import components in the script block and use them like HTML elements:
<!-- Page.luat -->
<script>
local Button = require("components/Button")
</script>
<div class="page">
<Button variant="primary" size="large">
Click me!
</Button>
<Button variant="secondary">
Cancel
</Button>
</div>
Try it live - Click the tabs to see both files:
Props
Passing Props
Props are passed as attributes on the component tag:
<script>
local Card = require("components/Card")
</script>
<Card
title="User Profile"
subtitle="Manage your account"
image="/avatar.jpg"
isActive={true}
/>
Spreading Props
You can use the spread operator (...) to pass all properties from a table to a component:
<script>
local Card = require("components/Card")
local cardProps = {
title = "User Profile",
subtitle = "Manage your account",
image = "/avatar.jpg",
isActive = true
}
</script>
<Card {...cardProps} />
This is particularly useful when:
- Forwarding props from a parent component
- Applying common properties to multiple components
- Building reusable component configurations
You can combine multiple spreads and override specific properties:
<script>
local baseProps = { size = "medium", variant = "default" }
local themeProps = { variant = "primary", outlined = true }
</script>
<!-- Later spreads override earlier ones, and direct props override all spreads -->
<Button {...baseProps} {...themeProps} size="large" />
See Template Syntax: Props Spread Operator for more details.
Accessing Props
Inside the component, access props via the global props table:
<!-- Card.luat -->
<script>
local title = props.title
local subtitle = props.subtitle or ""
local image = props.image
local isActive = props.isActive or false
</script>
<div class="card {isActive and 'active' or ''}">
{#if image}
<img src={image} alt={title} class="card-image">
{/if}
<div class="card-content">
<h3 class="card-title">{title}</h3>
{#if subtitle}
<p class="card-subtitle">{subtitle}</p>
{/if}
</div>
</div>
Default Props
Handle missing props with default values:
<script>
local variant = props.variant or "default"
local size = props.size or "medium"
local disabled = props.disabled or false
local children = props.children
</script>
Children
Rendering Children
Use {@render props.children?.()} to render content passed between component tags:
<!-- Modal.luat -->
<script>
local title = props.title
local isOpen = props.isOpen or false
</script>
{#if isOpen}
<div class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h2>{title}</h2>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
{@render props.children?.()}
</div>
</div>
</div>
{/if}
Using Components with Children
Pass content between the opening and closing tags:
<script>
local Modal = require("components/Modal")
</script>
<Modal title="Confirm Action" isOpen={showModal}>
<p>Are you sure you want to delete this item?</p>
<div class="modal-actions">
<button>Cancel</button>
<button class="danger">Delete</button>
</div>
</Modal>
Try it live - A simple Alert component with children:
Component Composition
Nested Components
Components can use other components:
<!-- UserCard.luat -->
<script>
local Card = require("components/Card")
local Button = require("components/Button")
local Avatar = require("components/Avatar")
</script>
<Card title={props.user.name} subtitle={props.user.role}>
<div class="user-card-content">
<Avatar src={props.user.avatar} size="large" />
<div class="user-info">
<p>Email: {props.user.email}</p>
<p>Joined: {props.user.createdAt}</p>
</div>
<div class="user-actions">
<Button variant="primary">Edit</Button>
<Button variant="secondary">View Profile</Button>
</div>
</div>
</Card>
Layout Components
Create reusable layout components:
<!-- Page.luat -->
<script>
local AppBar = require("components/AppBar")
local Footer = require("components/Footer")
local currentNode = getContext("currentNode")
local pageContext = getContext("pageContext")
</script>
<html>
<head>
<title>{currentNode.properties.title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="app">
<AppBar />
<main class="main-content">
{@render props.children?.()}
</main>
<Footer />
</div>
</body>
</html>
Component Context
Context provides a way to pass data through the component tree without having to pass props manually at every level. This is useful for sharing data like themes, user information, or application state that many components need access to.
Setting Context
Use setContext(key, value) to make data available to all child components:
<script>
-- Set context values that children can access
setContext("theme", "dark")
setContext("user", { name = "Alice", role = "admin" })
setContext("config", { language = "en", currency = "USD" })
</script>
<ChildComponent />
Getting Context
Use getContext(key) to retrieve context values set by parent components:
<script>
-- Access context from any parent component
local theme = getContext("theme")
local user = getContext("user")
local config = getContext("config")
</script>
<div class={"app theme-" .. (theme or "light")}>
Welcome, {user and user.name or "Guest"}!
</div>
How Context Works
Context in Luat uses a stack-based scoping system:
- Each component creates a new scope when it renders
setContext()writes to the current component's scopegetContext()searches from the current scope up through all parent scopes- When a component finishes rendering, its scope is removed
This means:
- Child components can access context set by any ancestor
- Sibling components cannot see each other's context
- Context values can be "overridden" by setting the same key in a child component
Context Flow Example
Here's a practical example showing context flowing through three levels of components. The App sets theme and user context, Dashboard adds navigation context, and UserCard (three levels deep) accesses all of them:
Context vs Props
Use props when:
- Data is specific to one component
- Parent needs to customize child behavior
- You want explicit data flow
Use context when:
- Many components at different levels need the same data
- Passing props through intermediate components is cumbersome ("prop drilling")
- Data is "global" within a subtree (theme, user, locale)
<!-- Props: explicit, local -->
<Button variant="primary" size="large">Click</Button>
<!-- Context: implicit, shared -->
<script>
setContext("theme", { variant = "primary", size = "large" })
</script>
<ThemedButton>Click</ThemedButton> <!-- reads theme from context -->
Framework-Provided Context
When using Luat with a framework like Wunderframe, certain context values are automatically available:
pageContext- Page-level data and configurationcurrentNode- Current content node datacurrentTheme- Active theme settings
<script>
local pageContext = getContext("pageContext")
local currentNode = getContext("currentNode")
</script>
<div data-theme={pageContext.currentTheme}>
<h1>{currentNode.properties.title}</h1>
</div>
Component Types
Module Exports
Components can export metadata using module scripts:
<!-- Hero.luat -->
<script module>
local type = "content:hero"
exports.type = type
</script>
<script>
local title = props.title
local subtitle = props.subtitle
local image = props.image
</script>
<div class="hero" style="background-image: url('{image}')">
<div class="hero-content">
<h1>{title}</h1>
<p>{subtitle}</p>
</div>
</div>
Advanced Patterns
Conditional Component Rendering
<script>
local Button = require("components/Button")
local Link = require("components/Link")
local ComponentToRender = props.href and Link or Button
</script>
<ComponentToRender href={props.href} onClick={props.onClick}>
{props.label}
</ComponentToRender>
Dynamic Block Rendering
A powerful pattern for CMS-style content is rendering different components based on block types. Here's how to render a blog post with various content blocks:
This pattern is useful for:
- CMS content - Render different block types (text, images, videos, embeds)
- Page builders - Dynamic layouts with configurable sections
- Form builders - Different input types based on field configuration
Component Factories
Create reusable HTML generators using factory functions. These return HTML strings that are rendered with {@html}:
<script>
local function createIcon(name)
return function(iconProps)
return string.format('<i class="icon icon-%s %s"></i>',
name, iconProps.class or "")
end
end
local HomeIcon = createIcon("home")
local UserIcon = createIcon("user")
</script>
<nav>
<a href="/home">{@html HomeIcon({class = "nav-icon"})}</a>
<a href="/profile">{@html UserIcon({class = "nav-icon"})}</a>
</nav>
Note: This pattern uses string-based HTML generation, not true component rendering. The factory function returns an HTML string, and
{@html}outputs it unescaped. For reusable UI elements with children support, use regular components instead.
Multiple Content Sections
Components can accept multiple render functions as props to create flexible layouts with header, body, and footer sections:
<!-- Card.luat -->
<script>
local title = props.title
</script>
<div class="card">
{#if title}
<div class="card-header">
<h2>{title}</h2>
</div>
{/if}
<div class="card-body">
{@render props.children?.()}
</div>
{#if props.footer}
<div class="card-footer">
{@render props.footer()}
</div>
{/if}
</div>
Usage with title prop and footer render function:
<script>
local Card = require("components/Card")
</script>
<Card title="User Profile" footer={function(__write)
__write("<button>Save</button>")
end}>
<p>This is the main card content.</p>
</Card>
Best Practices
1. Keep components focused
Each component should have a single responsibility:
<!-- Good: Focused button component -->
<!-- Button.luat -->
<button class="btn btn-{props.variant}" disabled={props.disabled}>
{@render props.children?.()}
</button>
2. Use descriptive prop names
<!-- Good -->
<UserCard
user={userData}
showActions={true}
onEdit={handleEdit}
/>
<!-- Avoid -->
<UserCard
data={userData}
flag={true}
callback={handleEdit}
/>
3. Provide sensible defaults
<script>
local variant = props.variant or "primary"
local size = props.size or "medium"
local disabled = props.disabled or false
</script>
4. Document complex components
<!--
UserProfileCard.luat
Props:
- user (object): User data with name, email, avatar
- editable (boolean): Whether to show edit controls
- onSave (function): Callback when profile is saved
- onCancel (function): Callback when editing is cancelled
-->
<script>
local user = props.user
local editable = props.editable or false
</script>