.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>Component files receive the same assigns map as Phoenix function components, but without requiring module boilerplate. Use assign/3, assigns_to_attributes/2, render_slot/1, and Phoenix's built-in <.dynamic_tag> for wrapper components:
<!-- components/width_wrapper.astral -->
---
assigns =
assigns
|> assign(:tag, assigns[:as] || "div")
|> assign(:class, assigns[:class])
|> assign(:rest, assigns_to_attributes(assigns, [:as, :class]))
---
<.dynamic_tag
tag_name={@tag}
class={[
"px-6 sm:px-8 md:max-w-screen-md xl:max-w-screen-lg md:px-12 mx-auto",
@class
]}
{@rest}
>
{render_slot(@inner_block)}
</.dynamic_tag><.width_wrapper as="section" id="projects" class="pt-8 pb-12 md:py-12">
...
</.width_wrapper>This is the HEEx equivalent of Astro's Astro.props, {...props}, <slot />, and dynamic <Tag> wrapper pattern.
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>Icons
Astral imports PhoenixIconify's <.icon> component into .astral templates. Use Iconify's prefix:name format and normal HEEx attributes:
<.icon name="ri:external-link-fill" class="inline-block mb-0.5" width="12" height="12" />Astral prepares the PhoenixIconify manifest during mix astral.build and development rendering, so sites do not need to add the :phoenix_iconify Mix compiler manually. Configure icon discovery at the intent level when needed:
config :phoenix_iconify,
source_globs: [
"pages/**/*.astral",
"components/**/*.astral",
"layouts/**/*.astral",
"content/**/*.md"
],
extra_icons: ["ri:external-link-fill"]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.vueThen 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 fromrequestIdleCallback, 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.
Islands can receive static HEEx children through the default framework slot/children channel. Astral keeps slot HTML separate from JSON props and passes it to the browser runtime as static HTML:
<.vue component="islands/Gallery.vue" props={%{images: @images}}>
<div class="thumbnail-strip">
<.image :for={image <- @images} src={image} alt="Office" height={320} />
</div>
</.vue>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. See the styling and browser code guide for details on how this differs from Astro's scoped styles, script processing, fonts, syntax highlighting, and framework components.