Svelte: my new obsession
I gave Svelte a try and ended up rewriting my whole site
Table of contents
I admit I am the kind of person who gets immediately obsessed with something if it is new and interesting. That’s how I got into Clojure, how I got into Rust, and how I got into the topic I’m writing about today: Svelte.
Svelte is not a new language I am learning, as it was with Rust and Clojure. It’s a JavaScript framework for building single page applications (SPA). Like React, Vue or Angular. Its syntax may remind some people of Vue; but it has a major difference with the typical framework. Svelte does not have a runtime and, unlike the rest, it’s not bundled with the rest of your code. Svelte is a compiler. It compiles your components into reactive JavaScript code that modifies the DOM directly. It will produce a small and more performant bundle.
How does it look?
Svelte tries to stay as close as possible to semantically correct HTML. This means that a simple HTML editor would do the trick for editing a Svelte component.
For example, the most minimalist component may look like this:
<p>Hello world!</p>
That’s it! That’s a valid Svelte component that the compiler will understand.
How to make it reactive?
Of course if you were going to write components like that, you would be better off writing plain HTML. The reason you’re using a JavaScript framework is because you need a way to handle reactivity in your web application. So we need a way to add JavaScript to our component. As you would do with plain HTML, JavaScript can be added inside a script
tag like this:
<script>
console.log('Hello world!');
</script>
In order to add “state” to our component we just need to declare JavaScript variables:
<script>
let message = 'Hello world!';
</script>
These variables can be accessed in the template by using curly braces. This is similar to how Vue does it.
<script>
let messsage = 'Hello world!';
</script>
<p>{message}</p>
The message
variable is reactive. If it were to change for some reason (e.g. an event), the contents of the p
tag would be immediately updated. For example, we can create an input that updates the content of message
.
The template of a Svelte component does not need to be a single element, so we can just add an input
element right after the p
element.
<script>
let message = 'Hello world!';
function onInput(event) {
message = event.target.value;
}
</script>
<p>{message}</p>
<input value="{message}" on:input="{onInput}" />
This would end up looking like this:
But this is still too complicated. Svelte provides some “magic” directives in order to make certain operations easier. The bind
directive helps with two-way data binding.
<script>
let message = 'Hello world!';
</script>
<p>{message}</p>
<input bind:value="{message}" />
This would result in the same output we saw above!
Svelte only handles reactivity on assignments. This means that array methods like
push
andpop
won’t trigger updates on the components.
Svelte also provides Handlebars like blocks to handle conditional rendering, asynchronicity and loops inside the templates.
What about styling?
Svelte provides a way to provide scoped styles to your components. As expected, this is done via the style
HTML tag. Svelte will assign unique classes to each of your component’s elements during compilation. You can use any valid CSS inside the tag, but a rollup/webpack plugin may be used to accept your favorite variant (e.g. SASS).
<h1>Hello world!</h1>
<style>
h1 {
color: purple;
}
</style>
On compile time, the h1
tag will be assigned a class generated by Svelte and a CSS selector for this class will be added to the h1
on the style
tag.
If you need to make the selector global you can wrap it in :global(...)
. For the example above, if we replaced h1
for :global(h1)
it would apply the style globally to all h1
elements of the project. This can be really useful if your project contains dynamically generated HTML not controlled by Svelte, since Svelte wouldn’t be able to assign unique classes to the elements inside it. Something like div :global(h1)
would select all h1
elements inside all div
elements of the component. This can be used to guarantee that the style remains scoped to the component.
Of course, you can always have a global CSS file to handle common styling for all components.
A more complete example
Here’s how a simple to-do app would look like in Svelte:
<script>
let todos = [],
value = '';
let filter = 'all';
// The $: tells Svelte to make the statement reactive.
// In this case, the assignment statement to "filtered" will be run
// everytime "todos" changes.
$: filtered = todos.filter((todo) => {
if (filter === 'checked') return todo.checked;
if (filter === 'unchecked') return !todo.checked;
return todo;
});
function addTodo() {
if (!value) return;
todos = [...todos, { value, id: Date.now(), checked: false }];
value = '';
}
</script>
<form>
<label for="all">
<input type="radio" id="all" value="all" bind:group="{filter}" />
All
</label>
<label for="checked">
<input type="radio" id="checked" value="checked" bind:group="{filter}" />
Checked
</label>
<label for="unchecked">
<input
type="radio"
id="unchecked"
value="unchecked"
bind:group="{filter}"
/>
Unchecked
</label>
</form>
<form on:submit|preventDefault="{addTodo}">
<input bind:value />
<button type="submit">Add Todo</button>
</form>
<ul>
{#each filtered as todo, i (todo.id)}
<li>
<input id="{todo.id}" bind:checked="{todo.checked}" type="checkbox" />
{todo.value}
</li>
{/each}
</ul>
<style>
label {
display: inline-block;
margin: 0 10px;
}
li {
list-style: none;
}
</style>
This example uses some features I didn’t talk about, but the official tutorial is great if you’re interested in learning more.
Other features
Svelte also provides some other nice features, such as:
- Built-in transitions and animations.
- Easily access the document’s head, window and body.
- Lifecycles for the components.
- Global stores.
- Compatibility with server side rendering.
- Components can be exported as web components.
Why rewrite the whole site?
Previously my site was written using Perun. It is a nice static site generator for Clojure that has tons of flexibility, since each step of the generation can be intercepted. But there were certain aspects of the generation that were hard to change or had little documentation. (I am not bashing on Perun, it is a perfectly great tool. I just wanted more freedom).
Perun generates a plain HTML output with no JavaScript. JavaScript needs to be injected manually. Each page is rendered by renderer functions written in Clojure which output HTML. Unless you installed extra packages, there is no built-in support for scoped styling. And, since the generator runs on top of the JVM, the generation of the site was quite slow.
This site is not written with plain Svelte. It is using Sapper. Sapper is a framework for Svelte inspired in Next.JS that allows to build server side rendered web applications. It can also export a static site like Next.JS does. This gives you much more freedom on how the site is generated at the cost of a bit more coding. For example, just as I did with Perun, each post’s content source is a markdown file. But for Sapper I manually had to write the process the reads the markdown files and generates the HTML. This allowed me to use libraries I am much more familiar with, such as Marked for markdown parsing and Highlight.js for code highlighting.
The resulting site works as an SPA and has some features that I couldn’t do previously, some of them:
- Highlighting for GraphQL code.
- Add working examples of code (such as the ones above).
- Navigate the whole site without having to do a page reload.
- Lazy loading of images.
- Embedding external elements to blog posts, like YouTube videos.
It also brought some DX improvements such as:
- Reusable components.
- Scoped CSS styling, which helped with some headaches I had previously due to my lack of CSS knowledge.
- Much faster generation of the static files.
- Easily add more interactive elements to the site. (I might add a search bar for my blog posts at a later time).
- It’s easier to follow a more maintainable code structure.
One downside to using Svelte is its lack of TypeScript support (although this is being worked on). Another downside is that Sapper is still in early development, so I would not recommend it for serious projects. Svelte itself is ready for production, though. It also has a much smaller ecosystem than other mainstream frameworks.
Conclusion
Even taking into account the downsides mentioned before, Svelte/Sapper has been a joy to work with. In under 20 hours of combined work I managed to rewrite my whole site. Svelte should be an excellent choice for performance critical web applications, and it’s syntax is easier to grasp compared to other frameworks. It should definitely not be considered a “toy” framework and I encourage you to add it to your tool set.
As a little extra, here’s the talk that got me excited about Svelte. I recommend everyone with a slight interest on Svelte to watch it.