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

ToolVersionWhy
Ruby3.1+Required by Rails 7.1+
Rails7.0.8+ or 7.1+ESBuild/jsbundling support
Node.js18 LTS or 20 LTSSvelte compiler, ESBuild
Yarn or npmYarn 1.22+ or npm 9+JS dependency management
jsbundling-rails1.2+Bridges ESBuild into Rails
svelte-on-railslatestView 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

Svelte

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 .svelte files
  • Configuration for ESBuild to process .svelte imports
  • 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 background fetch() 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.json alongside the Gemfile
  • 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

ScenarioOur recommendation
CRUD app, admin panel, content siteHotwire/Turbo + Stimulus
Real-time dashboard, data visualizationSvelte (or React if team already knows it)
Drag-and-drop, complex form wizardsSvelte
Existing Stimulus codebase, tight deadlineStick with Hotwire
New greenfield app, JS-heavy frontendConsider a separate SPA (SvelteKit/Next.js) with Rails API
Need SSR for SEO on dynamic pagesSvelteKit 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):

MetricHotwire/Turbo + StimulusSvelte islands
Total JS shipped to browser22 KB gzipped (Turbo + Stimulus)38 KB gzipped (14 Svelte components)
Time to interactive (Lighthouse, 4G throttle)1.2s1.1s
Server requests per user action1 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)120ms340ms
Build time (ESBuild, incremental)8ms25ms
Required infrastructureRuby onlyRuby + 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:

  1. Start with one page. Pick the most interactive view in your app (a dashboard, a settings page with lots of forms, a search interface).
  2. Extract one component. Turn the most interactive section into a Svelte component. Keep everything else as standard ERB.
  3. 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();
});
  1. 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.