Frontend Frameworks

Copy Markdown View Source

Volt has built-in support for Vue, Svelte, and React. No Node.js runtime is required — each framework's compiler runs through Rust NIFs or QuickBEAM.

React

React JSX and TSX files are transformed natively by OXC using the automatic JSX runtime.

Setup

# config/config.exs
config :volt,
  entry: "assets/js/app.tsx",
  sources: ["**/*.{js,ts,jsx,tsx}"],
  import_source: "react"

config :volt, :lint, plugins: [:typescript, :react]
// package.json
{
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

Entry Point

import { createRoot } from 'react-dom/client'
import App from './App'

const root = createRoot(document.getElementById('app')!)
root.render(<App />)

if (import.meta.hot) {
  import.meta.hot.accept()
}

Volt pre-bundles react, react-dom/client, and react/jsx-runtime into a single vendor module automatically.

Vue

Vue single-file components (.vue) compile through Vize, a Rust NIF wrapping the official Vue compiler. Scoped CSS, <script setup>, and TypeScript in SFCs are all supported.

Setup

# config/config.exs
config :volt,
  entry: "assets/js/app.ts",
  sources: ["**/*.{js,ts,jsx,tsx,vue}"],
  tailwind: [
    sources: [
      %{base: "lib/", pattern: "**/*.{ex,heex}"},
      %{base: "assets/", pattern: "**/*.{js,ts,jsx,tsx,vue}"}
    ]
  ]

config :volt, :lint, plugins: [:typescript, :vue]
// package.json
{
  "dependencies": {
    "vue": "^3.5.0"
  }
}

Entry Point

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

if (import.meta.hot) {
  import.meta.hot.dispose(() => app.unmount())
  import.meta.hot.accept()
}

Vapor Mode

Vue Vapor mode generates more efficient compiled output. Enable it in config:

config :volt, vapor: true

Svelte

Svelte components (.svelte) compile through QuickBEAM, which runs the Svelte compiler in-process without Node.js. Svelte 5 runes ($state, $derived, $props) are supported.

Setup

# config/config.exs
config :volt,
  entry: "assets/js/app.ts",
  sources: ["**/*.{js,ts,jsx,tsx,svelte}"],
  tailwind: [
    sources: [
      %{base: "lib/", pattern: "**/*.{ex,heex}"},
      %{base: "assets/", pattern: "**/*.{js,ts,jsx,tsx,svelte}"}
    ]
  ]

config :volt, :lint, plugins: [:typescript]
// package.json
{
  "dependencies": {
    "svelte": "^5.0.0"
  }
}

Entry Point

import { mount, unmount } from 'svelte'
import App from './App.svelte'

const target = document.getElementById('app')!
const app = mount(App, { target })

if (import.meta.hot) {
  import.meta.hot.dispose(() => unmount(app))
  import.meta.hot.accept()
}

Vanilla TypeScript

Volt works without any frontend framework. Use plain TypeScript with Phoenix LiveView hooks for interactivity — the same setup Phoenix uses by default, with Volt replacing esbuild.

See the vanilla example for a complete project with LiveView hooks, JSON imports, glob imports, and import.meta.env.

Examples

See the example apps for complete Phoenix projects using each framework.