Svelte is a relatively new UI framework developed by Rich Harris , who is also the author of the Rollup builder. Most likely, Svelte will seem completely different from what you have dealt with before, but perhaps this is even good. The two most impressive features of this framework are speed and simplicity. In this article, we will focus on the second.

Since my main development experience is related to Angular, it’s natural that I’m trying to learn Svelte by copying the approaches that are already familiar to me. And this is exactly what this article will be about: how to do the same things in Svelte as in Angular.
Note: Despite the fact that in some cases I will express my preference, the article is not a comparison of frameworks. This is a quick and easy introduction to Svelte for people who already use Angular as their main framework.
Spoiler alert : Svelte is fun.
Components
In Svelte, each component is associated with the file where it is written. For example, the Button
component will be created by naming the Button.svelte
file. Of course, we usually do the same in Angular, but with us it's just a convention. (In Svelte, the name of the imported component may also not coincide with the file name - note by the translator)
Svelte components are single-file, and consist of 3 sections: script
, style
and a template that does not need to be wrapped in any special tag.
Let's create a very simple component that shows "Hello World".

Component Import
In general, this is similar to importing a JS file, but with a couple of caveats:
- you must explicitly specify the
.svelte
component file .svelte
- components are imported inside the
<script>
<script> import Todo from './Todo.svelte'; </script> <Todo></Todo>
From the above snippets, it’s obvious that the number of lines for creating a component in Svelte is incredibly small. Of course, there are some implicitities and limitations, but at the same time everything is simple enough to quickly get used to it.
Basic syntax
Interpolation
Interpolations in Svelte are more similar to those in React than in Vue or Angular:
<script> let someFunction = () => {...} </script> <span>{ 3 + 5 }</span> <span>{ someFunction() }</span> <span>{ someFunction() ? 0 : 1 }</span>
I’m used to using double curly braces, so I’m sometimes sealed up, but maybe I only have this problem.
Attributes
Passing attributes to components is also quite simple. Quotation marks are optional and any Javascript expressions can be used:
//Svelte <script> let isFormValid = true; </script> <button disabled={!isFormValid}></button>
Developments
The syntax for event handlers is: on:={}
.
<script> const onChange = (e) => console.log(e); </script> <input on:input={onChange} />
Unlike Angular, we do not need to use parentheses after the function name to call it. If you need to pass arguments to the handler, just use the anonymous function:
<input on:input={(e) => onChange(e, 'a')} />
My view on the readability of such code:
- We have to print less, because we do not need quotation marks and brackets - this is good anyway.
- Read harder. I always liked the Angular approach rather than React, so for me and Svelte it’s more difficult to take. But this is just my habit and my opinion is somewhat biased.
Structural directives
Unlike structured directives in Vue and Angular, Svelte offers a special syntax for loops and branches inside templates:
{#if todos.length === 0} {:else} {#each todos as todo} <Todo {todo} /> {/each} {/if}
I love. No additional HTML elements are needed, and in terms of readability, this looks amazing. Unfortunately, the #
symbol in the British keyboard layout of my Macbook is in an inaccessible place, and this negatively affects my experience with these structures.
Input properties
Defining properties that can be passed to a component (analogous to @Input
in Angular) is as easy as exporting a variable from a JS module using the export
keyword. Perhaps at first it can be confusing - but let's write an example and see how really simple it is:
<script> export let todo = { name: '', done: false }; </script> <p> { todo.name } { todo.done ? '✓' : '✕' } </p>
- As you can see, we initialized the
todo
property along with the value: it will be the default value of the property, in case it is not passed from the parent component
Now create a container for this component, which will transmit data to it:
<script> import Todo from './Todo.svelte'; const todos = [{ name: " Svelte", done: false }, { name: " Vue", done: false }]; </script> {#each todos as todo} <Todo todo={todo}></Todo> {/each}
Similar to the fields in a regular JS object, todo={todo}
can be shortened and rewritten as follows:
<Todo {todo}></Todo>
At first it seemed strange to me, but now I think it's brilliant.
Output Properties
To implement the behavior of the @Output
directive, for example, when the parent component @Output
any notifications from the child, we will use the createEventDispatcher
function, which is available in Svelte.
- Import the
createEventDispatcher
function and assign its return value to the dispatch
variable - The
dispatch
function has two parameters: the name of the event and the data (which will fall into the detail
field of the event object) - We
markDone
dispatch
inside the markDone
function, which is called by the click event ( on:click
)
<script> import { createEventDispatcher } from 'svelte'; export let todo; const dispatch = createEventDispatcher(); function markDone() { dispatch('done', todo.name); } </script> <p> { todo.name } { todo.done ? '✓' : '✕' } <button on:click={markDone}></button> </p>
In the parent component, you need to create a handler for the done
event so that you can mark the necessary objects in the todo
array.
- Create the
onDone
function - We assign this function to the event handler that is called in the child component, as follows:
on:done={onDone}
<script> import Todo from './Todo.svelte'; let todos = [{ name: " Svelte", done: false }, { name: " Vue", done: false }]; function onDone(event) { const name = event.detail; todos = todos.map((todo) => { return todo.name === name ? {...todo, done: true} : todo; }); } </script> {#each todos as todo} <Todo {todo} on:done={onDone}></Todo> {/each}
Note: to start detecting changes in an object, we do not mutate the object itself. Instead, we assign the todos
variable a new array, where the object of the desired task will already be changed to completed.
Therefore, Svelte is considered truly reactive : with the usual assignment of a value to a variable, the corresponding part of the representation will also change.
ngModel
Svelte has a special bind:<>={}
syntax bind:<>={}
for binding specific variables to the attributes of a component and synchronizing them with each other.
In other words, it allows you to organize two-way data binding:
<script> let name = ""; let description = ""; function submit(e) { </script> <form on:submit={submit}> <div> <input placeholder="" bind:value={name} /> </div> <div> <input placeholder="" bind:value={description} /> </div> <button> </button> </form>
Reactive Expressions
As we saw earlier, Svelte responds to assigning values to variables and redraws the view. You can also use reactive expressions to respond to changes in the value of one variable and update the value of another.
For example, let's create a variable that should show us that in the todos
array all tasks are marked as completed:
let allDone = todos.every(({ done }) => done);
However, the view will not be redrawn when the array is updated, because the value of the allDone
variable allDone
assigned only once. We will use a reactive expression, which at the same time will remind us of the existence of "labels" in Javascript:
$: allDone = todos.every(({ done }) => done);
It looks very exotic. If it seems to you that there is “too much magic”, I remind you that tags are valid Javascript .
A small demo explaining the above:

Content injection
To embed content, slots are also used that are placed in the right place inside the component.
To simply display the content that was transferred inside the component element, a special slot
element is used:
// Button.svelte <script> export let type; </script> <button class.type={type}> <slot></slot> </button> // App.svelte <script> import Button from './Button.svelte'; </script> <Button> </Button>
In this case, the ""
will take the place of the <slot></slot>
element.
Named slots will need to be named:
// Modal.svelte <div class='modal'> <div class="modal-header"> <slot name="header"></slot> </div> <div class="modal-body"> <slot name="body"></slot> </div> </div> // App.svelte <script> import Modal from './Modal.svelte'; </script> <Modal> <div slot="header"> </div> <div slot="body"> </div> </Modal>
Lifecycle hooks
Svelte offers 4 lifecycle hooks that are imported from the svelte
package.
- onMount - called when a component is mounted in the DOM
- beforeUpdate - called before updating the component
- afterUpdate - called after component update
- onDestroy - called when a component is removed from the DOM
The onMount
function takes as a parameter a callback function that will be called when the component is placed in the DOM. Simply put, it is similar to the action of the ngOnInit
hook.
If the callback function returns another function, it will be called when the component is removed from the DOM.
<script> import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte'; onMount(() => console.log('', todo)); afterUpdate(() => console.log('', todo)); beforeUpdate(() => console.log(' ', todo)); onDestroy(() => console.log('', todo)); </script>
It is important to remember that when calling onMount
all the properties included in it must already be initialized. That is, in the fragment above, todo
should already exist.
State management
Svelte's state management is incredibly simple, and perhaps this part of the framework prefers me more than the rest. You can forget about the verbosity of code when using Redux. For an example, we will create storage in our application for storage and task management.
Recordable Vaults
First you need to import the writable
storage writable
from the svelte/store
package and tell it the initial value initialState
import { writable } from 'svelte/store'; const initialState = [{ name: " Svelte", done: false }, { name: " Vue", done: false }]; const todos = writable(initialState);
Usually, I put similar code in a separate file like todos.store.js
and export the storage variable from it so that the component where I import it can work with it.
Obviously, the todos
object has now become a repository and is no longer an array. To get the storage value, we’ll use a little magic in Svelte:
- By adding the symbol
$
to the name of the storage variable we get direct access to its value!
Thus, we simply replace in the code all references to the todos
variable with $todos
:
{#each $todos as todo} <Todo todo={todo} on:done={onDone}></Todo> {/each}
State setting
The new value of the recordable storage can be specified by calling the set
method, which imperatively changes the state according to the passed value:
const todos = writable(initialState); function removeAll() { todos.set([]); }
Status update
To update the storage (in our case, todos
), based on its current state, you need to call the update
method and pass it a callback function that will return the new state for the storage.
We rewrite the onDone
function that we created earlier:
function onDone(event) { const name = event.detail; todos.update((state) => { return state.map((todo) => { return todo.name === name ? {...todo, done: true} : todo; }); }); }
Here I used a reducer right in the component, which is bad practice. Let's move it to a file with our repository and export a function from it that will simply update the state.
Status Change Subscription
In order to find out that the value in the repository has changed, you can use the subscribe
method. Keep in mind that the repository is not an observable
object, but provides a similar interface.
const subscription = todos.subscribe(console.log); subscription();
Observables
If this part caused you the greatest excitement, then I hasten to please that not so long ago, RxJS support was added to Svelte and the Observable was introduced for ECMAScript.
As an Angular developer, I’m already used to working with reactive programming, and the lack of an async pipe counterpart would be extremely inconvenient. But Svelte surprised me here.
Let's look at an example of how these tools work together: display a list of repositories on Github found by the keyword "Svelte"
.
You can copy the code below and run it directly in the REPL :
<script> import rx from "https://unpkg.com/rxjs/bundles/rxjs.umd.min.js"; const { pluck, startWith } = rx.operators; const ajax = rx.ajax.ajax; const URL = `https://api.github.com/search/repositories?q=Svelte`; const repos$ = ajax(URL).pipe( pluck("response"), pluck("items"), startWith([]) ); </script> {#each $repos$ as repo} <div> <a href="{repo.url}">{repo.name}</a> </div> {/each}
Just add the $
symbol to the name of the observable variable repos$
and Svelte will automatically display its contents.
My wishlist for Svelte
Typescript Support
As a Typescript enthusiast, I cannot but wish for the possibility of using types in Svelte. I'm so used to it that sometimes I get addicted and arrange types in my code, which I then have to remove. I really hope that Svelte will soon add Typescript support. I think this item will be on the wish list of anyone who wants to use Svelte with experience with Angular.
Agreements and guidelines
Rendering in the representation of any variable from the <script>
block is a very powerful feature of the framework, but in my opinion, it can lead to code clutter. I hope that the Svelte community will work through a series of conventions and guidelines to help developers write clean and understandable component code.
Community support
Svelte is a grandiose project, which, with increased community effort in writing third-party packages, manuals, blog articles and more, can take off and become a recognized tool in the amazing world of Frontend development that we have today.
Finally
Despite the fact that I was not a fan of the previous version of the framework, Svelte 3 made a good impression on me. It is simple, small, but can do a lot. It is so different from everything around that it reminded me of the excitement that I experienced when I switched from jQuery to Angular.
Regardless of which framework you are using now, learning Svelte is likely to take only a couple of hours. Once you know the basics and understand the differences with what you’re used to writing, working with Svelte will be very easy.
In the Russian-language Telegram channel @sveltejs you will surely find developers who have experience with various frameworks and are ready to share their stories, thoughts and tips regarding Svelte.