Styling, Fonts, Scripts, and Frameworks

Copy Markdown View Source

Astral keeps document rendering in Elixir and delegates browser assets to Volt. This guide maps Astro's styling, font, script, syntax-highlighting, and frontend-framework features to Astral's current APIs.

CSS and styles

Use Volt-managed CSS for site-wide styles:

assets do
  entry "app.ts"
  url_prefix "/assets"
end
// assets/app.ts
import "./styles.css";

Reference the asset entry from a layout:

<script type="module" src="<%= Astral.asset_path(@site, "app.ts") %>"></script>

You can also place a <style> block directly in a .astral page, layout, or component:

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

Astral extracts .astral <style> blocks into Volt embedded modules. They are browser assets, not server-rendered inline CSS.

Astral does not currently implement Astro-style scoped CSS, is:global, :global(), class:list, or define:vars. Use HEEx and CSS directly:

<div class={["card", @featured && "card--featured"]}>
  {@title}
</div>

For public, unprocessed stylesheets, put files under public/ and link them normally:

<link rel="stylesheet" href="/styles/global.css">

Tailwind, PostCSS, and CSS preprocessors

Tailwind belongs to Volt. Configure a stylesheet root in Elixir:

config :volt, :tailwind,
  css: Path.expand("../assets/styles.css", __DIR__),
  name: "site",
  dev_url: "/assets/site.css"

Reference the source stylesheet from an Astral layout:

<link rel="stylesheet" href={Astral.asset_path(@site, "styles.css")} />

Astral supplies page, layout, component, collection, and asset source roots to Volt in development and production, preserving additional explicitly configured sources. The helper resolves the development URL or production manifest entry. No page-render compiler hook or site-specific Tailwind plugin is needed.

PostCSS and preprocessors remain browser-tooling concerns; configure only integrations supported by the installed Volt version.

Astral does not have an astro add tailwind equivalent. Keep the split explicit:

  • Astral config defines pages, content, routes, and site plugins.
  • Volt config and browser package files define CSS transforms, JS/TS checks, framework plugins, and asset behavior.

Fonts

Astral does not currently provide an Astro Fonts API or a font-provider abstraction.

Use ordinary web font patterns today:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
}

:root { --font-sans: "Inter", system-ui, sans-serif; }
body { font-family: var(--font-sans); }

Place font files in public/fonts/ when you want stable URLs, or import them from Volt-managed CSS when you want asset hashing. Add preload links in your head component or layout only for fonts that are critical for the first viewport:

<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>

A future font helper should be Elixir-native and privacy/performance-oriented rather than a direct JavaScript config clone.

Syntax highlighting

Astral Markdown is backed by MDEx. Code fences render as Markdown HTML, and you can style pre and code elements with CSS.

Astral does not yet expose a Shiki/Prism configuration surface or built-in <.code> / <.prism> components. For now, use CSS-only styling or a site/plugin-owned highlighter if your docs site needs richer code blocks.

Syntax highlighting defaults remain a starter-template/product-polish item on the roadmap.

Client-side scripts

Use .astral <script> blocks for page or component browser behavior:

<button data-confetti-button>Celebrate!</button>

<script lang="ts">
  document.querySelectorAll("[data-confetti-button]").forEach((button) => {
    button.addEventListener("click", () => console.log("celebrate"));
  });
</script>

Astral extracts these blocks into Volt embedded modules, so Volt handles TypeScript, imports, bundling, and dev-server behavior. Use standard DOM APIs and custom elements for static-site interactivity.

Server-side assigns are not browser variables. Pass data through HTML attributes when JavaScript needs per-element values:

<button data-message={@message}>Say hi</button>

For external scripts or files you want to serve exactly as written, place them under public/ and reference them with normal HTML:

<script src="/analytics.js" defer></script>

Frontend frameworks and islands

Astral supports client-only islands for Vue, Svelte, React, and Solid using Volt-managed framework compilation. Different framework islands can be mixed on the same .astral page:

<.vue component="islands/Gallery.vue" client={:visible} props={%{title: "Gallery"}} />
<.svelte component="islands/Newsletter.svelte" client={:idle} props={%{title: "Updates"}} />
<.react component="islands/ReactCounter.jsx" client={:load} props={%{count: 1}} />
<.solid component="islands/SolidBadge.solid.tsx" client={:media} media="(min-width: 640px)" props={%{label: "New"}} />

Supported client directives are :load, :idle, :visible, and :media. Media-gated islands hydrate as soon as their query matches, including after a viewport change. Island props must be JSON-shaped values or structs with explicit JSON encoding. Static HEEx children can be passed through the framework slot/children channel, and those children may include nested hydrated islands. Nested islands hydrate after their parent island finishes mounting.

Nested islands can cross framework boundaries. The child island entry may execute before its DOM exists or before the parent framework has inserted slot HTML; Astral waits for the parent island to finish hydrating before hydrating the child:

<.react component="islands/Shell.jsx" client={:load}>
  <.svelte component="islands/NestedButton.svelte" client={:load} props={%{label: "Buy"}} />
</.react>

A page can also repeat the same framework and use different loading strategies for each island. Production island entries are ES modules, allowing Volt to extract shared runtime/framework chunks for repeated islands when multi-entry shared chunks are available. Nested instances activate when their parent exposes the slot content, even if another instance already loaded the shared entry.

Astral collects island stylesheet dependencies across the page and its layout, including islands inside slot templates. After rendering, it emits deduplicated links in the active document head, in component-registration order with static dependency styles first. HTML documents requiring these links are parsed and serialized as HTML5 with an explicit doctype; fragments gain document structure. Non-HTML routes and documents with no island stylesheet dependencies are left untouched. Deduplication applies to collected dependencies, not author-supplied links, which may be conditional or disabled.

For example:

<section class="dashboard-widgets">
  <.vue component="islands/Chart.vue" client={:visible} props={%{id: "traffic"}}>
    <p>Static caption rendered by Astral.</p>
  </.vue>

  <.vue component="islands/Chart.vue" client={:load} props={%{id: "sales"}} />

  <.react component="islands/SearchBox.jsx" client={:load} props={%{placeholder: "Search"}} />
  <.react component="islands/FeedbackButton.jsx" client={:idle} props={%{label: "Feedback"}} />

  <.svelte component="islands/Newsletter.svelte" client={:idle} props={%{source: "footer"}} />
  <.solid component="islands/BreakpointBadge.solid.tsx" client={:media} media="(min-width: 768px)" props={%{label: "Desktop"}} />
</section>

Configure browser dependencies and framework plugins in Volt, not in Astral site plugins. Vue (.vue), Svelte (.svelte), and React JSX use Volt's built-in/default framework support. Solid JSX/TSX components should use the .solid.jsx or .solid.tsx filename convention so Astral can route those files through Volt's Solid compiler without changing how React .jsx and .tsx files are handled.

Current island support:

CapabilityStatus
Mix Vue, Svelte, React, and Solid islands on one pageSupported
Repeat multiple islands from the same frameworkSupported
Use :load, :idle, :visible, and :media on different islandsSupported
Pass static HEEx children into framework slot/children APIsSupported
Nest a hydrated island inside another island's slotSupported
Server-render framework components before hydrationNot supported yet
Hydrate .astral componentsNot supported

Astral does not currently SSR arbitrary framework components, hydrate .astral components, or provide client:only as a separate directive. The current island model is intentionally client-only while the native .astral and HEEx APIs stabilize.