Why Add Svelte to a Rails 7 App?
Rails 7 ships with Hotwire (Turbo + Stimulus) as the default frontend story. For most CRUD apps, that is enough. But some UIs need more: drag-and-drop builders, real-time dashboards, complex multi-step forms with client-side validation. That is where a component framework like Svelte earns its place.
Svelte compiles .svelte files into vanilla JS at build time. There is no runtime library shipped to the browser. A typical Svelte component compiles to 2-4 KB gzipped, compared to ~44 KB for React’s runtime alone. In a Rails app where you only need a few interactive islands, that difference matters.
What you will need
| Tool | Version | Why |
|---|---|---|
| Ruby | 3.1+ | Required by Rails 7.1+ |
| Rails | 7.0.8+ or 7.1+ | ESBuild/jsbundling support |
| Node.js | 18 LTS or 20 LTS | Svelte compiler, ESBuild |
| Yarn or npm | Yarn 1.22+ or npm 9+ | JS dependency management |
jsbundling-rails | 1.2+ | Bridges ESBuild into Rails |
svelte-on-rails | latest | View helper + SSR support |
Avoid Node 16 if possible. It hit end-of-life in September 2023 and several Svelte 4 dependencies emit deprecation warnings on it.
Svelte On Rails With Tailwind | Ruby On Rails 7

Create the Rails App with ESBuild
rails new svelte_rails_app --javascript=esbuild --database=postgresql
cd svelte_rails_app
The --javascript=esbuild flag installs jsbundling-rails and generates a package.json with an ESBuild build script. Skip the --css=bootstrap flag unless you actually need Bootstrap. Tailwind or plain CSS are lighter choices.
After creation, verify the build pipeline works:
bin/dev
This starts both the Rails server and the ESBuild watcher via Procfile.dev. Open http://localhost:3000 and confirm you see the Rails welcome page.
Gemfile essentials
Your Gemfile should already include:
gem "jsbundling-rails" # ESBuild integration
gem "turbo-rails" # Turbo Drive/Frames/Streams
gem "stimulus-rails" # Stimulus controllers
Add the Svelte integration gem:
gem "svelte-on-rails"
Then install:
bundle install
rails generate svelte_on_rails:install
The generator creates:
app/frontend/javascript/components/directory for.sveltefiles- Configuration for ESBuild to process
.svelteimports - A view helper
svelte_component()for rendering from ERB
Your First Svelte Component in Rails
Create app/frontend/javascript/components/HelloWorld.svelte:
<script>
export let name = "World";
export let count = 0;
function increment() {
count += 1;
}
</script>
<div class="hello">
<h2>Hello, {name}!</h2>
<button on:click={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
</div>
<style>
.hello {
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
button {
background: #4f46e5;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
</style>
Render it in any Rails view:
<%= svelte_component("HelloWorld", { name: "Rails Developer" }) %>
Restart bin/dev, visit the page, and you should see a working interactive counter. The component compiles to roughly 3.2 KB uncompressed JS.
Passing Data from Rails Controllers to Svelte
Prepare props in your controller, then pass them through the helper:
class DashboardController < ApplicationController
def show
@chart_data = {
labels: Transaction.last_12_months.pluck(:month),
values: Transaction.last_12_months.pluck(:total),
currency: "CHF",
updated_at: Time.current.iso8601
}
end
end
In the view:
<%= svelte_component("RevenueChart", @chart_data) %>
Rails serializes the hash to JSON automatically. Inside the Svelte component, each key becomes a prop:
<script>
export let labels = [];
export let values = [];
export let currency = "USD";
export let updated_at = "";
</script>
For locale-aware formatting (dates, currencies), use the browser’s Intl API inside Svelte rather than hardcoding formats. This keeps components reusable across locales.
USEO’s Take: When We Choose Svelte over Hotwire
We get asked about Svelte-in-Rails setups regularly. Here is our honest take after shipping both Hotwire and Svelte+Rails apps in production.
We default to Hotwire/Turbo for 80% of projects. The reason is simple: zero Node.js dependency, zero JS build step, and Rails 7’s Turbo Frames handle most interactive patterns (inline editing, modals, live search, tab navigation) without writing any JavaScript.
We evaluated Svelte for a client dashboard project that required a drag-and-drop Kanban board with optimistic updates, inline charts, and a multi-step wizard form with complex conditional logic. Turbo Frames could handle the data flow, but the UX felt sluggish because every interaction required a server round-trip. We prototyped both approaches:
- Hotwire version: 3 Stimulus controllers, 2 custom Turbo Stream actions, ~180 lines of JS. Drag-and-drop required a third-party library (
SortableJS) wired through Stimulus. Latency on drag operations was 200-400ms due to server round-trips. - Svelte version: 2 Svelte components, ~220 lines total. Drag-and-drop was native with
svelte-dnd-action. All state changes were instant on the client, synced to Rails via backgroundfetch()calls. Perceived latency: near zero.
We shipped the Svelte version.
The real cost of running two ecosystems
Adding Svelte to a Rails project means your team now maintains:
- A
package.jsonalongside theGemfile - A Node.js runtime in CI/CD and production Docker images (adds ~60 MB to image size)
- ESBuild or Vite configuration that can drift from Rails conventions
- Two testing worlds: RSpec/Minitest for Ruby, plus Vitest or Playwright for Svelte components
For a team of Rails developers who rarely touch JS frameworks, this overhead is real. We have seen teams add Svelte for one feature, then struggle to update dependencies six months later because nobody remembers how the JS build works.
Our decision framework
| Scenario | Our recommendation |
|---|---|
| CRUD app, admin panel, content site | Hotwire/Turbo + Stimulus |
| Real-time dashboard, data visualization | Svelte (or React if team already knows it) |
| Drag-and-drop, complex form wizards | Svelte |
| Existing Stimulus codebase, tight deadline | Stick with Hotwire |
| New greenfield app, JS-heavy frontend | Consider a separate SPA (SvelteKit/Next.js) with Rails API |
| Need SSR for SEO on dynamic pages | SvelteKit or Next.js as a separate app |
The worst outcome is mixing both everywhere. Pick Svelte for specific “islands” of interactivity and keep the rest in standard Rails views with Turbo.
Fetching Data from Rails APIs
For components that need fresh data after initial render, hit a Rails API endpoint:
# app/controllers/api/metrics_controller.rb
class Api::MetricsController < ApplicationController
def index
render json: {
revenue: Metric.current_month_revenue,
users: Metric.active_users_count,
updated_at: Time.current.iso8601
}
end
end
In Svelte:
<script>
import { onMount } from "svelte";
let metrics = null;
let error = null;
onMount(async () => {
try {
const res = await fetch("/api/metrics", {
headers: {
"Accept": "application/json",
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")?.content
}
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
metrics = await res.json();
} catch (e) {
error = e.message;
}
});
</script>
{#if error}
<p class="error">Failed to load metrics: {error}</p>
{:else if metrics}
<dl>
<dt>Revenue</dt>
<dd>{new Intl.NumberFormat("de-CH", { style: "currency", currency: "CHF" }).format(metrics.revenue)}</dd>
<dt>Active users</dt>
<dd>{metrics.users.toLocaleString()}</dd>
</dl>
{:else}
<p>Loading...</p>
{/if}
Note the CSRF token header. Without it, Rails rejects non-GET requests. For GET-only endpoints you can skip it, but include it as a habit for when you add POST/PATCH actions later.
Svelte vs Hotwire/Turbo: A Concrete Comparison
This is the comparison most Rails developers actually want. Here are measured numbers from a real project (a logistics dashboard with ~15 interactive widgets):
| Metric | Hotwire/Turbo + Stimulus | Svelte islands |
|---|---|---|
| Total JS shipped to browser | 22 KB gzipped (Turbo + Stimulus) | 38 KB gzipped (14 Svelte components) |
| Time to interactive (Lighthouse, 4G throttle) | 1.2s | 1.1s |
| Server requests per user action | 1 per interaction (Turbo Frame) | 0 for UI state, 1 for data sync |
| Lines of JS/Svelte code | ~400 (Stimulus controllers) | ~600 (Svelte components) |
| Build time (ESBuild, cold start) | 120ms | 340ms |
| Build time (ESBuild, incremental) | 8ms | 25ms |
| Required infrastructure | Ruby only | Ruby + Node.js |
Key observations:
- Hotwire wins on simplicity. Fewer moving parts, no Node dependency, smaller initial JS payload.
- Svelte wins on perceived performance. Client-side state updates feel instant. Hotwire always waits for a server response, even if it is fast.
- Svelte wins on complex UI patterns. Animations, drag-and-drop, optimistic updates, and conditional form logic are significantly easier in Svelte than in Stimulus.
- Hotwire wins on maintenance cost for teams that are primarily Ruby developers.
If your interactive needs are limited to “click a button, show updated HTML,” Hotwire is the better tool. If you need rich client-side state management, Svelte pays for its complexity.
Troubleshooting the Most Common Failures
”Cannot resolve module ‘svelte/compiler’”
This usually means a version mismatch between svelte-on-rails and the Svelte npm package. Check both:
bundle show svelte-on-rails # gem version
npx svelte --version # npm package version
Make sure the npm svelte package is version 4.x if using svelte-on-rails 1.x. Svelte 5 (runes) requires updated gem support.
ESBuild does not pick up .svelte files
Verify your esbuild.config.mjs (or build script in package.json) includes the Svelte plugin:
import esbuild from "esbuild";
import sveltePlugin from "esbuild-svelte";
esbuild.build({
entryPoints: ["app/javascript/application.js"],
bundle: true,
outdir: "app/assets/builds",
plugins: [sveltePlugin()],
});
Hot reload stops working
ESBuild’s watch mode sometimes loses track of .svelte files after a git branch switch. Kill the watcher and restart bin/dev. If the problem persists, increase your OS file watcher limit:
# macOS
sudo sysctl -w kern.maxfiles=524288
# Linux
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Production build fails with out-of-memory error
Large apps with many Svelte components can exceed Node’s default 512 MB heap. Set the limit explicitly:
NODE_OPTIONS="--max-old-space-size=4096" rails assets:precompile
Also ensure Node.js is available in your production environment or Docker image. A missing Node binary is the number one cause of silent asset compilation failures on deploy.
Adding Svelte Incrementally to an Existing Rails App
You do not need to rewrite your entire frontend. The best approach is to identify specific views that would benefit from client-side interactivity and replace only those sections with Svelte components.
A practical migration path:
- Start with one page. Pick the most interactive view in your app (a dashboard, a settings page with lots of forms, a search interface).
- Extract one component. Turn the most interactive section into a Svelte component. Keep everything else as standard ERB.
- Wire up Turbo compatibility. If you use Turbo Drive, reinitialize Svelte components on
turbo:load:
import { registerSvelteComponents } from "./svelte_loader";
document.addEventListener("turbo:load", () => {
registerSvelteComponents();
});
- Measure the impact. Compare Lighthouse scores before and after. If the Svelte component does not measurably improve the user experience, consider reverting to Hotwire.
FAQs
Does Svelte work with Rails 7 import maps?
No. Import maps do not support the compilation step that .svelte files require. You need a bundler like ESBuild or Vite. Use --javascript=esbuild when creating your Rails app, or migrate to jsbundling-rails if you started with import maps.
Can I use Svelte 5 (runes) with Rails?
Svelte 5 is usable with Rails via ESBuild, but the svelte-on-rails gem may need updates for full runes compatibility. Test with a small component first. The compilation step is the same; the difference is in Svelte’s reactivity syntax ($state instead of let, $derived instead of $:).
Should I use Svelte or React with Rails?
If your team already knows React, use React. The technical advantages of Svelte (smaller bundles, no virtual DOM) are real but modest for most apps. The bigger factor is team familiarity and hiring. Svelte is a better choice when you are starting fresh and want the smallest possible JS footprint.