How-To: Build UI with Islands and Pages
Vox relies on a server-first web architecture. Rather than building massive client-side bundles, Vox generates raw HTML routes and uses targeted interactive "islands" for dynamic functionality.
(Note: The legacy @island decorator has been removed in v0.3. Use @island and http get instead).
When to use @island vs http get
- Use
http get: When you need to return server-side rendered data, pages that require no Javascript, or raw API responses like JSON. - Use
@island: When the user needs to click, type, drag, or interact with state dynamically. Islands compile into hydrated React components under the hood.
Defining an Island with Props
Let's stick with the Task domain. Suppose you want a UI component to render a list of tasks.
// vox:skip
import react.use_state
@island
fn TaskList(tasks: list[Task]) -> Element {
let (items, set_items) = use_state(tasks)
<div class="task-list">
<h1>"Your Tasks"</h1>
<ul>
{items.map(fn(task) {
<li>{task.title}</li>
})}
</ul>
</div>
}
JSX Syntax within an Island
Within an @island body, the compiler supports standard JSX syntax.
- You can embed variables and functions within braces
{}. - You can include inline conditionals and standard attributes.
- Events like
onChangeoronClickare fully typed and bind directly to functions.
Calling @server Functions from an Island
The power of Vox is that your frontend and backend are co-located in the same file. You can call an @server function directly from a client-side button click without writing manual fetch() bindings!
// vox:skip
@server fn complete_task(id: Id[Task]) -> Result[Unit] {
db.Task.update(id, { done: true })
return Ok(())
}
@island
fn TaskRow(task: Task) -> Element {
<div class="task-row">
<input
type="checkbox"
checked={task.done}
onChange={fn(_e) complete_task(task.id)}
/>
<span>{task.title}</span>
</div>
}
The Vox compiler automatically generates the TypeScript client, handles the asynchronous RPC call, and returns the result back to your interactive component.
Passing Data from Server to UI
To get your database state into the TaskList, you map an endpoint directly to the UI component via the routes block. The system will automatically resolve queries to fulfill the tasks prop of TaskList.
// vox:skip
@query
fn get_active_tasks() -> list[Task] {
return db.Task.where({ done: false }).all()
}
routes {
// The framework will fetch `get_active_tasks` and inject the data
// into the `TaskList` component as props, then render to HTML.
"/" -> TaskList(tasks: get_active_tasks())
}
The Data/View routes { } Block
The routes block maps URL paths directly to server responses or UI.
// vox:skip
routes {
"/" -> HomeIsland # Render an Island
"/tasks" -> TaskList # Render the TaskList
"/dashboard" -> Dashboard # Render a complex page
}
AI-Generated Islands
[!TIP] Vox supports a special
@v0decorator for pulling down interface prototypes.@v0 "yM1xXq6" fn PricingTable() -> ElementThe orchestrator will dynamically download the requested implementation into
target/generated/at build time by calling Vercel's CLI. Use this pattern to integrate high-fidelity layouts without context switching.
Related Topics: