
Combining DHH’s vision
with modern front-end requirements:
Svelte.
Vite.
Hotwired.
Why Svelte?
On every app there are parts where you want it to shine. This is where Svelte comes in.
- Works perfectly with Hotwired/Turbo
- Perfect addition to Stimulus
- Easy to learn
- Powerful
- Super fast
- Compared to Stimulus
- No more writing double logic of the initial HTML state
- One Logic in one file!
- Render logic and script logic is in one file and, maybe, some css too.
- Stimulus is great for rails views with some javascript, but, complex parts: You will love svelte.
- Compared to React
- No more shadow dom and all those packages that are supposed to improve performance (e.g. useCallback…)
- Slimmer packages
- Easier to learn
- Faster
Have a look at this entertaining video rethinking reactivity
by Rich Harris, especially from 3:50 to 6:40 and his comparison to react.
This all fits in perfectly with the Rails way of minimalist javascript,
but: Where needed, we want full power.
Features
- ✍️ Sophisticated error messages
- 🚀 Cero-config deployment
- 🤝 Fully integrated with
assets:precompile
- 😍 Contributor friendly
Known Issues
see issues
Svelte on Rails 👍
Rock-solid and seamless integration of Svelte Components into Rails views, based on vite_rails
.
By default, and when installed together with @hotwired/turbo-rails
, it renders
svelte components on the first request server side («SSR») and for subsequent
requests it provides a empty tag which is mounted on the frontend
by the associated npm package @csedl/svelte-on-rails.
This way svelte works perfectly together with turbo. You will never notice
this unpleasant «blink» on the frontend while the whole process is maximum
performance optimized.
Thanks to shakacode
and ElMassimo for inspiring and helping me with their gems.
STATUS: This gem is in the early stages of development, but is ready for use.
It has nearly 100% test coverage and all tests pass. Since 15 May 2025, it has been successfully
implemented on my first customer’s app: an ERP system with fat Svelte components that are crucial for the workflow.
of this company.
If you have issues, please open one, and contributors are welcome!
Requirements
- actual node installed on the server
- tested on
- ruby 3.2.2 and rails 7.1
- ruby 2.7.5 and rails 6.1
- vite@6 (v7 not supported, see issues)
- vite_rails (the installer will install it by option –full or –vite)
- svelte v5 (see: how to install svelte on rails/vite)
- turbo (recommended / how to install turbo on rails)
- if you use special packages (like pug) that requires commonjs, you may need
- npm on latest versions
- actual node installed.
- if
nvm
is installed it gets the path to the node-binary from there. - When
.nvmrc
is present on projects root, it is respected - If node is not included on the PATH you can configure your node path by environment variable
SVELTE_ON_RAILS_NODE_BIN
- if
Installation from cero ⚙️
rails new my-test-app --skip-javascript
within the app add the gem
bundle add svelte-on-rails
and run the installer
rails g svelte_on_rails:install --full
You have done it! 👍
Start the server and you will see a svelte hello world component rendered on the browser.
Explanation:
The --full
contains:
--vite
- adds vite_rails gem and running the installer
--haml
- adds the gem and converts existing views
svelte_on_rails
- This is not a option, this is always done: Adds a config file and installs
@csedl/svelte-on-rails
by npm
- This is not a option, this is always done: Adds a config file and installs
--turbo
- installs
@hotwired/turbo-rails
and adds import statement to application.js
- installs
--svelte
- adds or updates
svelte
- adds or updates
--hello-world
- adds a hello world component
You can also use the options you want instead of using --full
.
And there is the option --force
that would not ask you whether it should overwrite existing files, for automated processes like testing.
This means, if you want all, except haml, you can run:
rails g svelte_on_rails:install --vite --turbo --svelte --hello-world
The installer is written carefully: It does not overwrite a file without asking
and tells you all what he is doing.
At the end, you just have to (re-) start your server
and you will see a Svelte Hello World component.
This hello world has a second page where the most common import statements are demonstrated,
separated for server and client side rendering.
Then, you can run
rails svelte_on_rails:remove_hello_world
and start coding.
Installation on a existing app ⚙️
Required: vite_rails
must be installed, it wants a app/frontend
folder.
Check Svelte Installation: install svelte on rails
Within the app, add the gem
bundle add svelte-on-rails
and run the minimal installer (you are free to add any options from above)
rails g svelte_on_rails:install
Restart the server, add a hello world component app/frontend/javascript/HelloWorld.svelte
like
export let title <h1>Svelte {title}</h1>
Add it to the view
<%= svelte_component('HelloWorld', title: 'Hello World') %>
And you should see “Svelte Hello World” on the browser! 👍 🤗
Explanation
this Minimal installer does:
- add
app/frontend/initializers/svelte.js
- Adds a import statement for that initializer to
application.js
- add
app/frontend/ssr/ssr.js
- add
config/svelte_on_rails.yml
- add
vite-ssr.config.ts
- add command
npm run build:ssr
to package.json - installs or updates npm packages to the latest:
@csedl/svelte-on-rails
typescript
@types/node
Troubleshooting
In the first step, the installer runs the backend parts that are unlikely to fail.
Then it installs the npm packages that are more likely to fail because of dependencies.
If so, just check that all the
packages are installed on the latest versions and you should be fine. 🤓
The most importand rule is to firstly check all npm packages installed and passing before changing your view logik.
Check if it all works
Server Side Rendering (SSR) is a parallel pipeline to client side rendering.
Both should return the same HTML. And your global styles should be applied same way
for both cases. For normal use cases this is.
For check the ssr pipeline you can pass the options ssr: true
and hydrate: false
to the view helper. This way you will see a «dead» backend rendered HTML with no javascript applied.
For check the client side pipeline you can pass the option ssr: false
and
hydrate: true
to the view helper.
If both are looking similar, you are good to go. Then, remove theese options, the defaults are
ssr: :auto
and hydrate: true
.
Import statements
The most importand import statements that are served by this gem are included in the
hello world component and by that they are also within the testing scope. So you can
be sure that they are working. If importand statements are missing there, pelase
tell me.
Among others, working statements are:
import svg from '../example.svg?raw'
- “ (svelte component with typescript syntax)
import { someFunction } from '../customJavascript.js';
import Child from './Child.svelte';
Precompile assets
Usual vite has a vite.config.ts
file, that is used for the client side precompilation.
By running this installer it adds vite-ssr.config.ts
and a npm runner so that you can do npm run build:ssr
which does the server side precompilation.
The same job is triggered alongside rails assets:precompile
for production environments.
On development, when watch_changes
is configured, the precompilation is triggered
after any *.svelte
file within the configured components_folder
changed.
Option ssr: :auto
ssr: :auto
is the default option, as set on config file and can be overridden on the view helper.
Works with hotwired/turbo
only
By passing the ssr: :auto
option to the view helper,
it checks if the request is an initial request (request header X-Turbo-Request-ID
is present):
On Initial-Request, it adds the attribute data-svelte-on-rails-initialize-action="hydrate"
and
returns a server side rendered component that will be hydrated on the frontend by the npm package.
Otherwise it adds mount
instead of hydrate, and renders a empty element, but provided with the
necessary attributes for the npm package.
More details to this point you can find on the npm package.
This works perfectly with hotwired/turbo because the javascript is only
loaded on very first load to the frontend, then the most work is done
in frontend and the server is relieved, except on initial request.
You will see no unpleasant «blink» on the page.
Tip: Performance optimisation for dropdowns
For example, with dropdowns, you will never see the unpleasant «blink» effect because
the Svelte component is not visible for the first moment after rendering.
Server-side rendering is unnecessary here. You can pass ‘ssr: false’ to the view helper.
This relieves the server and reduces loading time.
More rake tasks
This tasks are more for testing/playground purposes
rails svelte_on_rails:add_hello_world
rails svelte_on_rails:remove_hello_world
Deploy
For example, by deploying with capistrano, you may add a step like
before 'deploy:assets:precompile', 'deploy:npm:install' namespace :deploy do namespace :npm do desc 'Install Node.js dependencies' task :install do on roles(:app) do within release_path do execute :npm, 'install' end end end end end
to deploy.rb
for making sure the ssr compilation is working.
Contributors Guide
Contributors welcome!
Download the code and run the tests
Download the source code from the repository, and within the project folder run:
rake svelte_on_rails:create_contributor_configs_file
and define a generated_test_app_folder_path
(required) for apps, generated for the testings.
For development of the npm package @csedl/svelte-on-rails (optional) please download the source code of the
npm package and set local_npm_package_path
on the config file to the path to the npm package on your local machine.
This will cause the installer, to install the npm package from a local path instead from the npm registry.
Then run the tests and start contributing.
Tests are based on the included templates, like the hello world template
and on the installer.
Here I learned how to write tests! Testing installers is heavy, you will read about it in the
test_helpers. But I have done my best to give all the code a clear structure.
I hope you like it, and improvements are welcome.
Installer tests starting with completely destroy the rails app within the generated_test_app_path
,
generating a new rails app and running the installer and test by playwright
if the components are working.
component tests only checking if a rails server is alive, and if not, install and run a rails app.
For this is the testing helper start_rails_server_unless_ping
. This step may only be slow on the
first run, then it is fast. And on every repeating the test it always overwrites the components
with the components from the template by the testing helper install_hello_world(
. At the end of the test it leaves the rails server running.
['rails_vite_hello_world'],
app_root: generated_rails_app_root,
force: true,
silent: true
)
On that way a developer can just edit the templates and run a test and see always the refreshed
content on the browser and on the app within the generated_test_app_path
.
NOTE: Theese tests are dependend on your environment, including the running ruby version!
I am working on rvm. If you work on a different environment, some (not many) changes may be necessary.
Thats your part :)
The current test cases including (among others):
create a completely new rails app, running the full installer and check if a hello World component is visible and javascript is working.
run assets:precompile within a rails app and check if the gem does its precompiling too.
Understanding the process
Why not client and server rendering in one process?
That was my idea! application.js
which is the usual entry point for the client
side could live on the same assets and manifest like svelte components that are
compiled as chunks which each is its own entry point. This failed:
- The
vite-plugin-ruby
did not support this: it constrained all to one entry point. - See how fat the
vite-ssr.config.ts
is. For the client side this is not necessary.
At the end, I decided to split the process. For now it is cleaner. But that is not the last decision.
Why not compiling server side purely by rollup?
Advantages would be much slimmer packages and faster compilation. On the first
step i did this and i backed up a working and tested code on the branch rollup-ssr.
I decided to use Vite to bring the client and server side rendering
closer together.
But this, too, is not the last decision.
For now we proceed with vite.
How does it work now?
Client side rendering is done by vite like usual.
Server side rendering is triggered, similar to vite_rails
on assets:precompile
, and, if watch_changes
is configured,
which is default for development, it is triggered
on every change of a *.svelte
file within the configured components_folder
.
On the server side only the *.svelte
files are served. Theyr included
assets are linked to the client side assets folder, which is mapped by manifest.json
.
Then, vite has two output folders: vite-dev
for development and vite
for production.
Within vite-ssr.config.ts
, by the RAILS_ENV
variable, is decided which one is used.
Licence
License is MIT