.astral files are HEEx-first static templates. They can be used as pages, layouts, and local components.

Components

Place local components under the configured component directory, components/ by default:

<!-- components/pill.astral -->
<span class="pill">
  {render_slot(@inner_block)}
</span>

Use local components with HEEx syntax:

<.pill>Elixir</.pill>

Pages

.astral pages live in pages/:

---
assigns = assign(assigns, :title, "Home")
---

<h1>{@title}</h1>
<.pill>Static HTML</.pill>

The setup block is Elixir. It receives assigns and should return updated assigns when adding values.

Layouts

.astral layouts receive the same assigns as EEx layouts:

<!doctype html>
<html lang="en">
  <body>
    <main data-route={@route}>{@content}</main>
  </body>
</html>

HEEx syntax

Use Phoenix HEEx conventions:

<h1>{@title}</h1>

<ul>
  <li :for={item <- @items}>{item}</li>
</ul>

<p :if={@draft}>Draft</p>

Slots use HEEx slot rendering:

<div class="card">
  {render_slot(@inner_block)}
</div>

Client islands

Astral can mount client-only framework components from Volt-managed assets. All Volt framework adapters are enabled by default; configure islands do adapter :vue end only when you want to restrict the allowed set.

Place the browser component under your assets directory:

assets/islands/Gallery.vue

Then mount it from a .astral page or component:

<.vue
  component="islands/Gallery.vue"
  client={:load}
  props={%{images: @images}}
/>

Use <.vue>, <.svelte>, <.react>, or <.solid> for framework-specific islands. Supported client directives are:

  • :load — mount as soon as the entry module runs.
  • :idle — mount from requestIdleCallback, falling back to a short timeout.
  • :visible — mount when the island enters the viewport.
  • :media — mount only when a media query matches:
<.vue
  component="islands/Gallery.vue"
  client={:media}
  media="(min-width: 768px)"
  props={%{images: @images}}
/>

Props must be JSON-shaped data. Maps, lists, strings, numbers, booleans, nil, and atoms are accepted. Structs should either use JSONCodec or explicitly implement Jason.Encoder; unsupported values such as PIDs, references, and functions raise errors that include the component and prop path.

Astral writes a generated island entry module and Volt compiles the imported framework component, so framework compilation remains Volt-owned. The initial implementation is client-only; SSR hydration can be layered on later.

Browser assets

<style> and <script> blocks are extracted into Volt's asset graph:

<style>
  .hero { padding: 4rem; }
</style>

<script lang="ts">
  document.querySelector(".hero")?.classList.add("ready");
</script>

Astral removes those blocks from the server-rendered HTML template. Volt builds and serves them as first-class browser modules.