Table Of Contents
- Preface
- Introduction
- Use Cases
- Benefits & Drawbacks
- Project Setup
- Project Structure
- Component Structure
- State Management
- Props
- Data Binding
- DOM Binding
- Reference Binding
- Reactivity
- Reactivity Behavior
- Templating
- Keyed {#each} Block
- Events
- DOM Event Modifiers
- Component Dispatched Events
- Component Lifecycle
- Stores
- Custom Stores
- Slots
- Context API
- Special Elements
- Higher-order Components
- Module Context
- Transitions & Animations
- Motion
- Transition Application
- Transition Events
- Custom Transitions
- Key Blocks
- Transition Modifiers
- Deferred Transitions
- Animate Directive
- Actions
- CSS Preprocessor Integration
- Tailwind CSS Integration
- Lazy Loading
- Consuming APIs
- Forms & Validation
- Documenting
- Debugging
- Testing
- Routing
- TypeScript Limitations
- Deploying
- Runes
- Conclusion
Preface
This tutorial provides a comprehensive overview of Svelte 4, detailing all its aspects. Basic knowledge of HTML, CSS, and JavaScript is necessary to follow this tutorial effectively. Moreover, having familiarity with frontend frameworks would be beneficial for a better understanding of the concepts presented.
Note that this tutorial does not include coverage of SvelteKit.
All the examples used in this post can be found in this GitHub repository
Introduction
Svelte is a JavaScript framework used for building user interfaces. What sets Svelte apart from other frameworks is that it compiles your code at build time rather than running it in the browser. This means that the resulting code is highly optimized and efficient, resulting in faster load times and better performance for your applications.
During development, Svelte code is written using its component-based syntax and reactive programming model. Svelte analyzes the code and compiles it into optimized JavaScript code. When the application is ready for deployment, the building process starts. A bundle that can be served to the browser is generated. The bundle typically includes the optimized JavaScript, CSS, and other necessary assets. Once the build is complete, the generated bundle is deployed to a web server. When a user visits your site, their browser downloads and executes the compiled JavaScript code.
It is important to highlight that Svelte provides official support for TypeScript and includes built-in animation and transition capabilities, eliminating the need for external libraries.
Additionally, as you explore Svelte, you may encounter SvelteKit, the official framework built on top of Svelte. While Svelte serves as a standalone framework for UI development, SvelteKit expands its capabilities by offering additional features and tools.
Use Cases
Svelte excels in single-page applications (SPAs), providing smooth user experiences. Its reactive nature enables interactive UI components responsive to user actions. It is also well-suited for data visualization, rapid prototyping, and progressive web applications due to its simplicity, quick setup, and small bundle size, ensuring fast loading times and offline capabilities.
A single-page application (SPA) is a type of application where a single HTML file is loaded for all server requests.
Benefits & Drawbacks
Some key benefits of using Svelte include:
Component-based Architecture: Svelte promotes a component-based architecture, allowing you to build reusable, self-contained and modular components. This helps in organizing your codebase and enables easier collaboration among developers. Additionally, it encourages a clear separation of concerns, making your code more maintainable and scalable.
Performance: Svelte takes a different approach than virtual DOM-based frameworks. It is a compiler that analyzes your code during build time and generates optimized JavaScript code that directly manipulates the DOM. This means that updates and changes are handled at compile time, resulting in faster and more efficient rendering at runtime. By eliminating the need for a virtual DOM, Svelte can offer improved runtime performance and reduced overhead.
Reactivity: Svelte provides a built-in store system that allows you to manage and update the application's state in a reactive manner. With stores, you can define and subscribe to reactive data, and any changes to that data will automatically trigger updates in the UI. This makes it easier to keep your UI synchronized with the underlying data and ensures that it reflects the latest state of the application.
Minimalism: Minimalism refers to the design philosophy of keeping things simple and concise. Svelte follows this principle by providing a syntax that is designed to be concise and readable. With Svelte, the same functionality can be achieved with less code compared to other frameworks, which can lead to improved readability and a reduced learning curve.
Framework Size: Svelte itself is lightweight, which means it does not come bundled with a lot of additional dependencies. This can make your development process simpler by reducing the project complexity. By keeping the core framework lightweight, it gives you more control and allows you to make informed decisions about which additional tools or libraries to include. This flexibility lets you choose only the specific features you need, rather than dealing with a potentially bloated ecosystem.
Bundle Size: Svelte compiles your code during the build process, resulting in highly optimized JavaScript code and smaller bundle sizes compared to other frameworks. Smaller bundle sizes lead to faster load times, as there is less data to transfer over the network, and improved performance, as the browser can parse and execute the code more quickly.
Compatibility: Svelte is compatible with numerous JavaScript/TypeScript libraries and tools, aligning with the established standards of the JavaScript/TypeScript ecosystem.
Accessibility Checks: Although Svelte does not include built-in accessibility checks, it does offer warnings during compile time if you create inaccessible markup.
While Svelte has many benefits, like any framework, it also has some potential drawbacks:
Small(er) Community & Ecosystem: The Svelte ecosystem is not as extensive as others. While Svelte may not have as large of an ecosystem as some of the more established frameworks, it has been steadily growing in popularity. There are several third-party libraries and tools specifically built for Svelte, such as Svelte Material UI, Svelte Charts, and Svelte Testing Library, among others. These libraries provide additional functionality and convenience for developing Svelte applications.
Developer Experience (DX): Svelte introduces unique concepts and syntax that may feel different from other frameworks. While Svelte utilizes familiar HTML, CSS, and JavaScript/TypeScript, it has its own way of constructing UIs. Instead of relying on a virtual DOM or manual state management, Svelte introduces reactivity at a language level. This reactive approach can be a paradigm shift for developers. Moreover, component composition is achieved using slots and props, rather than relying on a parent-child relationship. Additionally, it takes a compile-time approach, where components are compiled into efficient JavaScript code during the building process. This differs from frameworks like React, which perform updates at runtime.
No Older Browser Support: Svelte utilizes modern features like reactive updates and JavaScript modules, which offer efficient and dynamic updates to UI. However, it is important to note that not all browsers fully support these modern features. Older browsers may have limited or no support for them. While Svelte itself does not have built-in support for older browsers, Babel can be utilized in the build process to ensure compatibility across a wider range of browsers. Babel is a JavaScript compiler that can transpile modern code into a format that older browsers can interpret. By using Babel with Svelte, you can transform your code into a compatible format, allowing you to leverage Svelte's modern features while still supporting older browsers. Additionally, if you choose to use Vite to build your Svelte code, you will not need to worry about additional configuration for Babel. Vite uses Babel under the hood to handle the transpilation process automatically.
Tooling: Svelte lacks an official CLI tool for project scaffolding, unlike other frameworks. Nevertheless, community-driven solutions like degit enable the quick setup of a new Svelte project with a basic folder structure. On the debugging and testing front, Svelte provides a dev mode with useful error messages and warnings during development. Furthermore, there are dedicated testing frameworks such as Svelte Testing Library tailored for unit testing Svelte components.
Project Setup
To create a Svelte project there are 2 main methods.
The first method involves using degit, a tool for cloning Git repositories.
After installing degit via npm install degit
, you can run npx degit sveltejs/template my-app
to create a new Svelte project named my-app. This command downloads the contents of the Svelte project template from the GitHub repository called sveltejs/template. If you prefer to use TypeScript, you can specify the TypeScript branch with --branch typescript
.
After creating the project, navigate to the folder my-app and run npm install
to install dependencies, followed by npm run dev
to start the development server. If you are using TypeScript, run node scripts/setupTypescript.js
, inside my-app folder, before installing dependencies. You can replace my-app with your desired project name.
The second method, which is considered the modern approach, involves using npm to create a Svelte project with Vite, a modern build tool for web development. By executing npm create vite@latest
, you can initialize a new project with Vite, providing a streamlined setup process with built-in support for Svelte and modern build features like fast development server and optimized production builds. With Vite, you can choose between JavaScript and TypeScript when creating your Svelte project, offering flexibility to tailor the project setup to your preferences. This command fetches the latest version of Vite from the npm registry and guides you through configuration options.
Note that both commands create a basic Svelte project without SvelteKit. After setting up your project, you can run npm run dev
to start the development server.
npm install degit
npx degit sveltejs/template my-app
cd my-app
npm install
npm run dev
Create JavaScript project with degit
npm install degit
npx degit sveltejs/template my-app --branch typescript
cd my-app
node scripts/setupTypescript.js
npm install
npm run dev
Create TypeScript project with degit
npm create vite@latest
cd my-app
npm install
npm run dev
Create JavaScript/TypeScript project with Vite
Project Structure
In a Svelte project, the src folder typically includes 2 key files, main.js
, or main.ts
for TypeScript, and App.svelte
.
The main.js
or main.ts
file acts as the entry point for the application, responsible for configuring and initializing Svelte components. It manages tasks like mounting the root component onto the DOM and handling application-level functionality.
On the other hand, the App.svelte
file functions as the primary component that lays the groundwork for the application's UI. It defines the overall layout and structure, importing and rendering other components as necessary. Each Svelte component has its own scoped CSS styles defined within the component file. Additionally, Svelte projects often include a file for application-wide styles.
For projects created with degit, this file can be found under public/global.css
, while for Vite projects, it is typically located under src/app.css
.
Svelte offers flexibility in naming conventions and file structure, allowing developers to organize their code according to the specific requirements of their project.
Component Structure
A component is a reusable and modular building block that encapsulates a part of the UI. Components can range from simple elements like buttons or input fields to more complex elements like navigation bars or modals. They help in organizing the UI into smaller, manageable parts, promoting code reusability and maintainability. Components have their own structure, styling, and behavior, making it easier to develop and maintain large-scale web applications. In Svelte, components are defined in .svelte
files. These files contain 3 main sections: script, markup, and style.
The script section is where you write your JavaScript/TypeScript code. Here, you can define variables, create functions, import modules, or even other components, and handle component logic. When importing other components, any name beginning with a capital letter is valid. It is recommended to use Pascal casing for the names for consistency and readability. Note that for a TypeScript project, you can specify the language by adding a lang='ts'
property to the script section.
The style section is where you define the CSS styles for the component. You can write regular CSS rules to style your component's elements. It is important to note that in Svelte, these sections are tightly integrated. This means that you can reference variables and functions defined in the script section directly in the markup and style sections.
Additionally, it is worth mentioning that these sections can be written in any order or even omitted if they are not needed for a particular component. Everything that is not within the style or script sections is considered markup. The markup section contains the HTML markup for your component, where you can use regular HTML tags and Svelte-specific syntax to define the structure and content of your component. By combining HTML, CSS, and JavaScript/TypeScript in one file, Svelte simplifies development and encourages component reusability.
<script>
let name = 'World';
</script>
<p>Hello, {name}</p>
<style>
p {
font-weight: 600;
}
</style>
A simple JavaScript component example featuring all sections
<script lang='ts'>
let name: String;
</script>
<p>Hello, {name || 'World'}</p>
<style>
p {
font-weight: 600;
}
</style>
A simple TypeScript component example featuring all sections
State Management
State refers to the data that represents the current condition or snapshot of an application at any given moment. It includes information such as user inputs, fetched data, selected options, and any other relevant data that affects the behavior and appearance of the application.
State management involves techniques, patterns, and tools that allow developers to handle and update this state in a predictable and efficient manner. Svelte utilizes stores as reactive data containers that facilitate state sharing and observation within components. By defining reactive variables and storing values in these stores, you can ensure automatic state updates when changes occur. This centralized method streamlines state management and enhances code organization.
Further information about stores will be provided later.
Props
In component-based frameworks like Svelte, props serve as a fundamental mechanism for establishing communication between different components. They allow for the passing of data from parent components to child components, enabling the creation of modular and reusable pieces of code.
Child components are components that are integrated into a parent component's markup. The parent component includes these child components, which are designed to work together within the parent's structure. This approach promotes a clear separation of concerns, with each component being responsible for a specific part of the UI or functionality.
To define props in a component, the export
keyword is utilized for each prop declaration in the script section. Props can either have default values or be left undefined. By passing values to the child component's props from the parent, specific data or behavior can be provided to customize the child component's functionality.
The child component can access and utilize these prop values in its markup or script sections. It is worth noting that any modifications made to the props within the child component solely impact the local copy and do not change the original values passed by the parent.
<script>
export let username, email;
export let group = 'Users';
export let status;
/**
* $$props is a special object that contains
* all the props passed to a component as key-value pairs.
* $$restProps is used to access any additional props
* passed that are not explicitly defined in the
* component to its child.
* $$props include $$restProps, but not vice versa.
*/
if (Object.entries($$restProps).length) {
console.log('props:', $$props);
console.log('restProps:', $$restProps);
}
</script>
<div class='container'>
<p>Username: {username}</p>
<p>Email: {email}</p>
<p>Group: {group}</p>
<p>Status: {status}</p>
</div>
<style>
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
margin-left: 1em;
padding: 1em;
border: 2px solid;
}
</style>
User.svelte
<script>
import User from './User.svelte';
const userEmail = 'user2@mail.com';
const status = 'Online';
const user = {
username: 'User 4',
email: 'user4@mail.com',
status
};
</script>
<div class='container'>
<!-- Passing all props -->
<User
username='User 1'
email='user1@mail.com'
group='Super Users'
status='Offline'
/>
<!--
Passing all props except for the "group" prop,
which has a default value.
"email" prop receives the value of variable userEmail.
"status" prop receives the value of variable status.
-->
<User
username='User 2'
email={userEmail}
status={status}
/>
<!-- "{status}" is shorthand for "status={status}" -->
<User
username='User 3'
email='user3@mail.com'
{status}
/>
<!-- Spread props -->
<User {...user} />
<!--
User with extra props.
The "prop=value" syntax passes values as a string.
When the value is enclosed in {value},
it will retain its original type.
-->
<User
{...user}
arr={[1, 2, 3]}
obj={{ a: 1, b: 2, c: 3 }}
bool={true}
num={123}
/>
</div>
<style>
.container {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 1em;
}
</style>
Profile.svelte
This example illustrates how props are passed between a parent component named Profile and a child component called User.
Within the User Component, props for username, email, group, and status are declared, with the group prop having a default value of "Users" for fallback.
The Profile component demonstrates prop passing methods such as direct assignment, variable utilization, and prop spreading via the spread operator.
It is important to note the difference between the
prop=value
syntax, which passes values as strings, and enclosing values in{value}
to maintain their original data type.Additionally, the User component makes use of the special variables
$$props
and$$restProps
. The$$props
variable holds all the passed props, while$$restProps
allows access to any extra, undeclared props. If$$restProps
are present, these values are logged into the console.
Data Binding
Parent components serve as higher-level components responsible for managing the state and behavior of one or more child components. They transmit data to child components via props, which are variables or values accessible to and utilized by the child components. In contrast, child components function as lower-level components that receive data from their parent components through props to render content or execute specific tasks. Child components can trigger events or call functions in their parent components to communicate changes and update shared data. There are 2 approaches for updating data between components, uni-directional and bi-directional binding.
Uni-directional binding, or one-way binding, enables data to flow solely from a parent component to a child component. Changes in the parent component are reflected in the child component, maintaining a structured data flow.
Conversely, bi-directional binding, or two-way binding, facilitates data exchange between a parent and child component bi-directionally. Modifications in either component trigger updates in both, ensuring synchronized data. This is beneficial for scenarios where immediate updates in one component should be mirrored in the other.
When a parent component passes a value through props to a child component, and the child component calls a parent component function with that value, it creates a scenario where the 2 values reference the same data. This can create a form of two-way binding because changes in one component affect the other. If the data passed between components is different or unrelated, it establishes 2 distinct one-way bindings, one from the parent component to the child component and another from the child component to the parent component. Each component operates independently in this situation, with changes in one component not directly impacting the other unless explicitly programmed to do so.
It is important to note that when a parent component passes a function through props to a child component, and the child component uses this function to send data back to the parent component, it adheres to uni-directional data flow principles.
Svelte promotes uni-directional binding, which improves clarity and simplifies application debugging. However, it also provides features like event handling or the bind:
directive to facilitate bi-directional binding when necessary.
<script>
export let description, count;
export let updateValue = null;
function increment() {
count++;
typeof updateValue === 'function' && updateValue(count);
}
</script>
<div>
<p>Child with {description}</p>
<p>Count: {count}</p>
<button on:click={increment}>Increment</button>
</div>
<style>
div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
Child.svelte
<script>
import Child from './Child.svelte';
let count = 0;
</script>
<div class='container'>
<div class='parent'>
<p>Parent</p>
<p>Count: {count}</p>
<button on:click={() => count++}>Increment</button>
</div>
<!-- "{count}" is shorthand for "count={count}" -->
<Child
description='one-way binding'
{count}
/>
<!-- "bind:count" is shorthand for "bind:count={count}" -->
<Child
description='two-way binding'
bind:count
/>
<Child
description='2 one-way bindings resulting in a two-way binding'
{count}
updateValue={cnt => count = cnt}
/>
</div>
<style>
.container {
display: flex;
flex-wrap: wrap;
gap: 1em;
justify-content: center;
align-items: center;
}
/**
* :global() targets global CSS styles, applying them
* universally across components
*/
.container :global(div) {
padding: 1em;
margin: 1em;
}
.container :global(div):not(.parent) {
border: 2px solid red;
}
.parent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 2px solid green;
}
</style>
Parent.svelte
In the example above, the Parent component initializes a variable named
count
with an initial value of0
. Thiscount
variable is then passed down to the Child components as a prop calledcount
, establishing a one-way binding. Within the Parent component, there are 3 instances of the Child component.The first Child component receives the
count
prop, showcasing a one-way binding. This means that any modifications made to thecount
variable in the Parent component will be reflected in the first Child component. However, changes made within the first Child component will not impact the Parent component.In contrast, the second Child component utilizes the
bind:
directive in thecount
prop, creating a two-way binding. This two-way binding allows changes made to thecount
variable within the second Child component to affect the Parent component as well.The third Child component receives both the
count
prop and anupdateValue
prop, which is a function intended to update thecount
variable in the Parent component. This setup results in a situation where the values in both components reference the same data, leading to a form of two-way binding where modifications in one component will influence the other.If the data passed between the components were distinct or unrelated, it would demonstrate 2 separate one-way bindings, one from the Parent to the Child and another from the Child to the Parent. In such a scenario, each component would function independently, with changes in one component not directly impacting the other unless specifically programmed to do so.
An important aspect to consider is that updating the
count
variable in the Parent component will lead to a re-rendering of all Child components that receive thecount
prop. Consequently, Child components that are two-way bound will update all other Child components.
DOM Binding
The bind:
directive can be used to establish a two-way binding between DOM elements and your component's data, ensuring that any changes made in the DOM elements automatically update the component's data, and vice versa. Additionally, utilizing the bind:group
feature allows you to group radio or checkbox inputs related to the same value, with radio inputs offering exclusive selection and checkbox inputs enabling multiple selections.
<script>
let value = 'Default value';
let numValue, rangeValue;
let checked = true;
let selected;
let selectedOptions = [];
let checkedOptions = [];
let selectedRadioOption;
</script>
<div class='container'>
<div class='item'>
<!-- Binding between attributes and component properties -->
<p>Text: {value}</p>
<!-- "bind:value" is shorthand for "bind:value={value}" -->
<input bind:value />
<textarea bind:value></textarea>
</div>
<div class='item'>
<!--
Svelte automatically handles
the conversion of data types
-->
<p>
{numValue || 0} +
{rangeValue || 'Drag Slider'} =
{(numValue + rangeValue) || '---'}
</p>
<input type='number' bind:value={numValue} />
<input type='range' bind:value={rangeValue} />
</div>
<div class='item'>
<label>
<!--
"bind:checked" is shorthand for
"bind:checked={checked}"
-->
<input type='checkbox' bind:checked />
Toggle Button
</label>
<button disabled={!checked}>Button</button>
</div>
<div class='item'>
<p>Selected: {selected}</p>
<select bind:value={selected}>
<option value='A'>A</option>
<option value='B'>B</option>
<option value='C'>C</option>
</select>
</div>
<div class='item'>
<p>Selected: {selectedOptions.join(', ')}</p>
<select multiple bind:value={selectedOptions}>
<option value='A'>A</option>
<option value='B'>B</option>
<option value='C'>C</option>
</select>
</div>
<!--
Bind a group of checkbox inputs,
allowing multiple selections
-->
<div class='item'>
<p>Checked: {checkedOptions.join(', ')}</p>
<label>
<input
type='checkbox'
value='A'
bind:group={checkedOptions}
/>
A
</label>
<label>
<input
type='checkbox'
value='B'
bind:group={checkedOptions}
/>
B
</label>
<label>
<input
type='checkbox'
value='C'
bind:group={checkedOptions}
/>
C
</label>
</div>
<!--
Bind a group of radio buttons,
allowing only one selection at a time
-->
<div class='item'>
<p>Checked: {selectedRadioOption || '---'}</p>
<label>
<input
type='radio'
value='A'
bind:group={selectedRadioOption}
/>
A
</label>
<label>
<input
type='radio'
value='B'
bind:group={selectedRadioOption}
/>
B
</label>
<label>
<input
type='radio'
value='C'
bind:group={selectedRadioOption}
/>
C
</label>
</div>
</div>
<style>
div {
display: flex;
flex-wrap: wrap;
gap: 1em;
justify-content: center;
align-items: center;
}
.container {
flex-direction: row;
width: 80%;
}
.item {
flex-direction: column;
padding: 1em;
border: 2px solid green;
}
</style>
DOM.svelte
The code example above demonstrates data binding implementation with various DOM elements. It initially binds an input field and a textarea to a variable named
value
, ensuring changes in one element reflect instantly in the other. Thevalue
variable's content is dynamically displayed in real-time, showcasing the seamless connection between these elements.Additionally, it binds a number input and a range input to variables
numValue
andrangeValue
respectively, and calculates their sum. Svelte automatically handles string to number conversion.Moreover, a checkbox is bound to a variable
checked
, enabling the seamless toggling of a button based on the checkbox's status. This showcases how data binding can create interactive UIs that respond to user actions.A select element is bound to the variable
selected
, showing the currently selected option.A multi-select element is bound to an array
selectedOptions
, displaying all selected choices.Furthermore, the code binds a group of checkboxes to an array
checkedOptions
, displaying all currently checked checkboxes.Lastly, a group of radio buttons is bound to the variable
selectedRadioOption
, allowing only one selection at a time. This enforces exclusivity among radio options.
Reference Binding
Reference binding, facilitated by bind:this
, empowers you to connect DOM elements and component references to variables, providing direct access to their properties and methods. This capability simplifies interaction and manipulation of components and DOM elements.
<script>
export let content = '';
export function clear() {
content = '';
}
</script>
<p>{content}</p>
Paragraph.svelte
<script>
import Paragraph from './Paragraph.svelte';
let content;
let p, div;
</script>
<div class='container'>
<div class='item'>
<textarea bind:value={content}></textarea>
<button on:click={p.clear}>Clear Paragraph</button>
<!-- Bind to component reference -->
<Paragraph bind:content bind:this={p}></Paragraph>
</div>
<!-- Bind to DOM element -->
<div class='item' bind:this={div}>
<button on:click={() => div.style.backgroundColor='red'}>
Change Color
</button>
</div>
</div>
<style>
div {
display: flex;
flex-wrap: wrap;
gap: 1em;
justify-content: center;
align-items: center;
}
.container {
flex-direction: row;
width: 80%;
}
.item {
flex-direction: column;
padding: 1em;
border: 2px solid green;
}
</style>
ReferenceBinding.svelte
The code example above demonstrates 2 distinct uses of the
this
binding. Initially, it binds a component reference by associating the Paragraph component instance with the variablep
. This connection grants direct access to the properties and methods of the Paragraph component through thep
variable.As a result, clicking the Clear Paragraph button triggers the invocation of the clear method of the Paragraph component.
Note that the
bind:content
binding is used to populate the Paragraph component with the content provided in the textarea.Furthermore, the
this
binding is employed to bind the div DOM element to the variablediv
. This linkage enables direct manipulation of the DOM element using thediv
variable. Consequently, clicking the button within this div alters the background color of the div to red.
Reactivity
Reactivity is a core feature of the Svelte framework, ensuring that the DOM reflects changes in the application's state in real-time. This synchronization keeps the UI current with data modifications triggered by user interactions or other events. Svelte's reactivity system functions by tracking dependencies between variables and the UI elements that rely on them. When a variable changes, Svelte smartly updates only the necessary parts of the DOM to show the new value, avoiding the need to re-render the entire component. This targeted updating approach reduces unnecessary computations, boosts performance, and provides a smooth user experience. Svelte employs various techniques for achieving reactivity, including reactive assignments, reactive statements, and reactive declarations.
Reactive assignments trigger automatic updates in the DOM when a variable in the markup is reassigned.
On the other hand, reactive declarations involve prefixing a variable or expression with $:
to establish a subscription to the variable. This subscription ensures that any modifications to the variable lead to a re-rendering of the component, keeping the UI current.
It is important to differentiate between reactive assignments, which handle variable reassignment in the markup, and reactive declarations, which are variables that update when the corresponding right-hand side variable changes.
Reactive statements, identified by $:
preceding a code block, subscribe to a variable within the block, leading to the execution of the code when that variable changes.
While reactive declarations and reactive statements are sometimes used interchangeably, it is essential to understand their unique purposes. Reactive declarations create variables, while reactive statements execute code blocks based on variable changes.
It is worth noting that Svelte's reactive syntax resembles JavaScript labels in the label: expression
format but introduces additional concepts beyond standard JavaScript.
<script>
let message = 'Initial value';
let counter = 0;
// Reactive declaration
$: doubled = 2*counter + 1;
// Single reactive statement
$: console.log('Counter is', counter);
// Multiple reactive statements
$: {
if (counter > 10)
message = 'More than 10';
else if (counter > 5)
message = 'More than 5';
else
message = 'Less than or equal to 5';
}
</script>
<p>{message}</p>
<p>Counter: {counter}</p>
<p>Doubled + 1: {doubled}</p>
<!--
Reactive assignment.
"counter++" translates to "counter = counter + 1" under the hood.
-->
<button on:click={() => counter++}>Increment</button>
Reactivity.svelte
The code above demonstrates the concept of reactivity in Svelte using different mechanisms. It initializes a
counter
variable to0
and amessage
variable toInitial value
.The line
$: doubled = 2*counter + 1;
sets up a reactive declaration, ensuring that whenever thecounter
variable changes,doubled
will update to twice the value ofcounter
plus one.The line
$: console.log('Counter is', counter);
represents a single reactive statement, which logs the current value ofcounter
to the console whenevercounter
changes.Within the
$: {}
block, there are multiple reactive statements that determine the value of themessage
variable based on the currentcounter
value. Themessage
variable updates reactively based on the conditions specified in the block.Upon initialization, the reactive declaration and reactive statements are triggered.
In the markup, the values of
message
,counter
, anddoubled
are displayed and automatically update when the corresponding variables change. Initially,message
displays Less than or equal to 5,counter
shows 0, anddoubled
is 1 due to the triggering of reactivity. Additionally, "Counter is 0" will be logged to the console. The button Increment increases the "counter" variable by 1 when clicked, triggering the reactive updates.
Reactivity Behavior
The reactivity system in Svelte is designed to automatically handle most common scenarios. However, there are specific cases where reactivity may not trigger as expected. One such instance is when modifying arrays using methods like push and splice. To ensure reactivity in such cases, you can add an assignment to the array after modification, or use the spread operator to create a new array instead of modifying the existing one.
Similarly, when modifying nested properties of an object through a reference object, reactivity may not be triggered automatically. To address this, you can ensure reactivity by reassigning the object variable.
In cases where functions in the markup contain variables that have been changed, Svelte may not detect these changes and trigger a re-render.
Additionally, if a function parameter shares the same name as a top-level variable, the parameter may shadow the variable, leading to unexpected behavior. To maintain proper reactivity, it is recommended to use distinct names for variables and parameters.
<script>
let stack = [0, 1, 2];
let n = 3;
</script>
<p>n: {n}</p>
<p>Stack top: {stack.at(-1)}</p>
<button on:click={() => stack.push(++n)}>
Non-working Push
</button>
<button on:click={() => stack = [...stack, ++n]}>
Working Push
</button>
Array.svelte
In the code above, the
stack
variable is initialized as an array[0, 1, 2]
, andn
is set to3
. The current value ofn
, representing the next value to be added to the stack, is displayed, along with the top element of the stack. Two buttons, Non-working Push and Working Push are present.The Non-working Push button pushes the incremented value of
n
into thestack
array but fails to update the displayed stack top. In contrast, the Working Push button correctly updates the UI by adding the incrementedn
value to a new array created using the spread operator.The issue with the Non-working Push button arises from directly mutating the original stack array using push, which does not trigger Svelte's reactivity system to update the UI. Conversely, the Working Push button creates a new array by spreading the original stack elements and adding the incremented
n
value. This method correctly updates the stack array, leveraging Svelte's reactivity based on assignment to reflect changes in the UI.
<script>
const user = {
location: {
country: 'France'
}
};
let nonWorkingLocation = user.location;
let workingLocation = user.location;
let value;
function nonWorkingSetCountry(country) {
user.location.country = country;
}
function workingSetCountry(country) {
user.location.country = country;
workingLocation = user.location;
}
</script>
<p>{user.location.country}</p>
<p>{nonWorkingLocation.country}</p>
<p>{workingLocation.country}</p>
<input bind:value />
<button on:click={() => nonWorkingSetCountry(value)}>
Non-working Set Country
</button>
<button on:click={() => workingSetCountry(value)}>
Working Set Country
</button>
Object.svelte
In the code above, the
user
object contains a nestedlocation
object with acountry
property set to France. Two variables,nonWorkingLocation
andworkingLocation
, are initialized to reference theuser.location
object. Avalue
variable is declared to store the input value.There are 2 functions,
nonWorkingSetCountry
, which updates theuser.location.country
directly with the input country, andworkingSetCountry
, which updates theuser.location.country
with the input country and assigns the updateduser.location
toworkingLocation
.In the markup, the country of the
user.location
object,nonWorkingLocation
, andworkingLocation
are displayed. An input field is provided for entering a new country value. Two buttons, Non-working Set Country and Working Set Country, trigger the respective functions to update the country value in theuser.location
object. The difference between the Non-working Set Country and Working Set Country functions lies in how they handle updating theworkingLocation
variable.
The Non-working Set Country function directly modifies the user.location.country
, while the Working Set Country function updates both user.location.country
and then reassigns workingLocation
to reference the updated user.location
object. This distinction affects reactivity. Updating the reference of workingLocation
triggers reactivity, ensuring that changes are reflected in the UI, while not reassigning the reference object does not trigger reactivity.
<script>
let stack = [0, 1, 2];
let n = 3;
let topVariable = stack.at(-1);
$: topReactDecl = stack.at(-1);
function topFn() {
return stack.at(-1);
}
</script>
<p>n: {n}</p>
<p>Stack top: {stack.at(-1)}</p>
<p>Stack top (Variable): {topVariable}</p>
<p>Stack top (Reactive Declaration): {topReactDecl}</p>
<p>Stack top (Function): {topFn()}</p>
<button on:click={() => stack = [...stack, ++n]}>
Push
</button>
Function.svelte
In this code example, there is an array named
stack
initialized as[0, 1, 2]
, and the variablen
is set to3
. The current value ofn
indicates the next value to be added to the stack.A variable
topVariable
is defined to reference the top element of the stack usingstack.at(-1)
.There is also a reactive declaration named
topReactDecl
, which tracks the value of the top element of the stack with the expression$: topReactDecl = stack.at(-1)
.Additionally, a function named
topFn
is created to return the value of the top element of the stack by usingstack.at(-1)
.Within the markup, the values of
n
and the various methods to access the top of the stack are displayed. A button labeled Push is included to trigger an event that adds the incremented value ofn
to thestack
array by creating a new array with the spread operator. This click event triggers reactivity for all approaches except fortopVariable
andtopFn()
.Reactivity is not triggered for
topVariable
andtopFn()
because they are not reactive in nature. ThetopVariable
variable simply references the top element of the stack usingstack.at(-1)
when it is declared. It does not have any mechanism to automatically update its value when the stack changes. ThetopFn()
function returns the value of the top element of the stack when called. Similar totopVariable
, it does not have built-in reactivity to update its value when the stack changes.On the other hand,
topReactDecl
is a reactive declaration that explicitly tracks the value of the top element of the stack using the$:
syntax. This means that whenever the stack changes,topReactDecl
will automatically update its value to reflect the new top element of the stack.In the case of
{stack.at(-1)}
, the displayed value is dependent on the top element of the stack. As a result, whenever the top element of the stack changes, Svelte recognizes this dependency and updates the displayed value accordingly.
<script>
function nonWorkingLogout(user) {
user = { ...user, status: 'offline' };
}
function workingLogout(loggedUser) {
user = { ...loggedUser, status: 'offline' };
}
let user = {
username: 'user',
status: 'online'
};
</script>
<p>{user.username} is {user.status}</p>
<button on:click={() => nonWorkingLogout(user)}>
Non-working Logout
</button>
<button on:click={() => workingLogout(user)}>
Working Logout
</button>
Shadowing.svelte
In the code example above, there is a
user
object with propertiesusername
set touser
andstatus
set toonline
. Two functions,nonWorkingLogout
andworkingLogout
, are defined.In the nonWorkingLogout function, the parameter is named user. Inside the function, a new object is created by spreading the properties of the user parameter and adding a new key-value pair to change the status to offline. However, due to variable shadowing, the
user
variable within the function is local and does not affect the outeruser
variable defined outside the function.On the other hand, in the workingLogout function, the parameter is named loggedUser. The object is updated similarly to the nonWorkingLogout function. In this case, the new object is assigned to the variable
user
, which is not locally defined in the function. This action successfully updates the outeruser
variable defined outside the function.In the markup, the username and status of the
user
object are displayed.Two buttons are provided, with one triggering the nonWorkingLogout function and the other triggering the workingLogout function upon being clicked. When the Non-working Logout button is clicked, the status of the
user
object does not change to offline as intended due to variable shadowing. Conversely, clicking the Working Logout button successfully updates the status of the user object to offline because the outeruser
variable is modified within the function.
Templating
Svelte's templating system provides a concise and intuitive approach for developing reactive web applications. In component markup, the {variable}
syntax is used for variable interpolation, allowing dynamic content rendering with reactive updates. The {@html variable}
special tag permits the injection of raw, unsanitized HTML content, but caution is advised to prevent potential XSS vulnerabilities.
Svelte offers features for conditional rendering, looping, and promise handling within templates. Conditional rendering is achieved using the {#if}
block statement to render code based on specific conditions, with support for {:else if}
and {:else}
statements for multiple conditions. The {#each}
block statement enables iteration over arrays or iterable objects for markup rendering. For promise handling, the {#await}
block statement manages promises with separate blocks for pending, resolved, and rejected states.
The {@const expression}
special tag defines local constants for use within {#if}
, {:else if}
, {:else}
, {#each}
, {:then}
, and {:catch}
blocks, enhancing readability and efficiency by avoiding repetitive calculations.
Svelte also includes the class: directive for dynamic CSS class inclusion, allowing adjustments to styling based on conditions. The style: directive enables the transfer of CSS variables to CSS properties, facilitating dynamic styling of DOM elements. Additionally, through component styles, Svelte promotes reusability and consistency by passing CSS variables to child components, ensuring a cohesive design experience throughout the application.
It is worth noting that CSS is contained within the component to prevent unintended style clashes. When styling elements beyond a component's boundaries is necessary, the :global()
directive can be used to extend CSS rules throughout the application. It is advisable to use it sparingly to minimize CSS leakage and maintain encapsulation. Limiting the use of :global()
enhances styling encapsulation and helps prevent unintended style conflicts between components.
<script>
const item = '<strong>Hello, world</strong>'
</script>
<p>Render as text: {item}</p>
<p>Render as HTML: {@html item}</p>
Example of variable interopolation
<script>
let count = 0;
</script>
<p>Count: {count}</p>
<button on:click={() => count++}>Increment</button>
<!-- Both "else if" and "else" blocks are optional -->
{#if count > 10}
<p>More than 10</p>
{:else if count >= 5}
<p>More than or equal to 5</p>
{:else}
<p>Less than 5</p>
{/if}
Example of conditional rendering
<script>
const users = [
{ username: 'user1', status: 'online'},
{ username: 'user2', status: 'offline'},
{ username: 'user3', status: 'online'}
];
</script>
<!-- else block is optional -->
{#each users as user}
<p>{user.username} is {user.status}</p>
{:else}
<p>No users to display</p>
{/each}
{#each users as user, index}
<p>{index}. {user.username} is {user.status}</p>
{/each}
Example of iteration over array to render each element
<script>
const users = [
{ username: 'user1', age: 25, location: { country: 'USA' } },
{ username: 'user2', age: 35, location: { country: 'UK' } },
{ username: 'user3', age: 32, location: { country: 'USA' } },
{ username: 'user4', age: 23, location: { country: 'UK' } },
{ username: 'user5', age: 21, location: { country: 'UK' } },
{ username: 'user6', age: 44, location: { country: 'USA' } },
];
</script>
<!-- Object destruction-->
{#each users as { username, age, location }, index}
<!-- Local constants -->
{@const country = location.country}
{#if country === 'USA'}
{@const text = `${username}, ${age} is`}
<p>{index + 1}. {text} from {country}</p>
{/if}
{/each}
Example of using object destruction and local constants in the markup
<script>
const promiseThatResolves = new Promise(resolve => {
setTimeout(
() => resolve('Hello, world'),
2000
);
});
const promiseThatRejects = new Promise((_, reject) => {
setTimeout(
() => reject('An error occured'),
2000
);
});
</script>
<!-- Handle pending and resolved states-->
{#await promiseThatResolves}
<p>1. Waiting to resolve...</p>
{:then data}
<p>1. Resolved to "{data}"</p>
{/await}
<!-- Handle resolved state -->
{#await promiseThatResolves then data}
<p>2. Resolved to "{data}"</p>
{/await}
<!-- Handle pending, resolved, and rejected states -->
{#await promiseThatRejects}
<p>3. Waiting to resolve...</p>
{:then data}
<p>3. Resolved to "{data}"</p>
{:catch error}
<p>3. Rejected with "{error}"</p>
{/await}
<!-- Handle resolved and rejected states -->
{#await promiseThatRejects then data}
<p>4. Resolved to "{data}"</p>
{:catch error}
<p>4. Rejected with "{error}"</p>
{/await}
Example of rendering based on promise status
The example above illustrates the
class:
directive,style:
directive, and component styles:
<script>
export let label;
let toggled = true;
</script>
<!-- Toggle class without class directive -->
<button
on:click={() => toggled = !toggled}
class={toggled ? 'state-true' : 'state-false'}
>
{label}
</button>
<!-- Toggle class with class directive -->
<button
on:click={() => toggled = !toggled}
class='state-true'
class:state-false={!toggled}
>
{label}
</button>
<!-- Toggle class with multiple class directives -->
<button
on:click={() => toggled = !toggled}
class:state-true={toggled}
class:state-false={!toggled}
>
{label}
</button>
<!-- Shorthand class directive -->
<button
on:click={() => toggled = !toggled}
class:toggled
>
{label}
</button>
<style>
.state-true {
background-color: var(--color-true);
}
.state-false {
background-color: var(--color-false);
}
.toggled {
background-color: brown;
}
</style>
ToggleButtons.svelte
<script>
import ToggleButtons from './ToggleButtons.svelte';
</script>
<!-- Using component styles -->
<ToggleButtons
label='Click'
--color-true='green'
--color-false='red'
/>
<!-- Using style directive on DOM element -->
<p style:--text-color='blue'>Click any button</p>
<style>
p {
color: var(--text-color);
}
</style>
Styling.svelte
An example of leaking CSS using the
:global()
directive:
<p>Child Component</p>
Child.svelte
<script>
import Child from './Child.svelte';
</script>
<p>Parent Component</p>
<Child />
Parent.svelte
<script>
import Parent from './Parent.svelte';
</script>
<div>
<Parent />
</div>
<style>
/*
Limit :global() rules as much as possible.
Apply to p elements that follow another
p element and have a div parent.
*/
div > :global(p + p) {
background-color: red;
color: #fff;
padding: 1em;
margin: 1em;
}
</style>
Global.svelte
The
div > :global(p + p)
rule targets the Child component.
Keyed {#each} Block
In an {#each}
block, the key attribute uniquely identifies each array item during rendering in Svelte. This helps Svelte efficiently update and re-render the list when changes occur. Although the array index can serve as a key in some cases, it may not always guarantee uniqueness, especially with dynamic array manipulation. It is recommended to associate the key attribute directly with the object itself by accessing a property that is guaranteed to be unique. This ensures that each item has a distinct identifier. Providing a key allows Svelte to map each rendered item to a specific identifier. When the array is updated, Svelte uses these keys to efficiently determine which blocks need to be removed from the DOM. If a key is no longer present in the updated array, the corresponding item is removed from the DOM. However, if the key remains the same and only the properties change, Svelte will update the properties of the existing item instead of recreating it. To prevent unintended behavior during array manipulation, it is recommended to utilize keyed {#each}
blocks.
<script>
// Updated when prop changes in parent component
export let current;
// Set during component creation and not updated afterwards
let initial = current;
console.log(`ColorBadge (${initial}, ${current}) created`);
</script>
<p>
<span style='background-color: {initial}'>Initial</span>
<span style='background-color: {current}'>Current</span>
</p>
<style>
p {
display: flex;
justify-content: center;
align-items: center;
gap: 0.2em;
color: #fff;
font-weight: 700;
}
span {
width: 4em;
margin: 0.2em;
padding: 0.3em;
border-radius: 0.2em;
text-align: center;
}
</style>
ColorBadge.svelte
<script>
import ColorBadge from './ColorBadge.svelte';
let colors = [
{ id: 1, value: 'red' },
{ id: 2, value: 'orange' },
{ id: 3, value: 'green' },
{ id: 4, value: 'blue' },
{ id: 5, value: 'brown' }
];
function deleteFirst() {
colors = colors.slice(1);
}
</script>
<div class='container'>
<div>
<h2>Keyed</h2>
{#each colors as color (color.id)}
<ColorBadge current={color.value} />
{/each}
</div>
<div>
<h2>Non-keyed</h2>
{#each colors as color}
<ColorBadge current={color.value} />
{/each}
</div>
</div>
<div class='container mt'>
<button on:click={deleteFirst}>Delete First</button>
</div>
<style>
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 5em;
}
.mt {
margin-top: 1em;
}
</style>
KeyedEach.svelte
The code example above demonstrates the use of keyed
{#each}
blocks. It includes a ColorBadge component with 2 variables,current
, which updates with changes in the parent component, andinitial
, which is set during component creation and remains constant, storing color values.Two spans in a paragraph display background colors based on these variables. Additionally, a message is logged in the console when the component is created. The KeyedEach component uses an array of colors to create ColorBadge components.
The colors are shown in 2 sections, one using the keyed approach and the other using the non-keyed approach. There is a Delete First button that removes the first item in the color array, triggering a UI re-render. In this scenario, ColorBadge components are not destroyed and recreated, instead, they are updated. The console.log will not trigger.
When the array is updated, the keyed approach correctly displays ColorBadge components. However, in the non-keyed approach, the old value for initial is displayed in each ColorBadge as items shift in the array. This issue arises from Svelte's inability to detect the dependency between the initial and current variables, leading to a mix-up in the non-keyed approach.
Events
Events are handled using the on:
directive, which allows Svelte to listen for both DOM events and custom events on elements. These events serve as communication units between different elements and components. Custom events are particularly useful for transmitting data between child and parent components. When using the on:event-name
directive, a function can be assigned as the value, which will be invoked after the event is dispatched. The function receives an event object containing information about the event. It is important to note that events can be triggered by various activities on the web page, such as clicks, mouse movements, and keyboard inputs.
<button
on:click={() => console.log('Hello, world')}
>
Click
</button>
<div
on:mouseenter={() => console.log('Hovered')}
on:mouseleave={() => console.log('Unhovered')}
>
Hover Over Here
</div>
Examples of event handling
DOM Event Modifiers
Modifiers in DOM event handlers offer enhanced control and customization options, allowing for more precise handling of events. These modifiers grant extra control in managing event behavior.
For instance, the preventDefault modifier can be used to invoke e.preventDefault()
before executing the handler, preventing the default event behavior. The stopPropagation modifier stops the event from propagating to the next element by calling e.stopPropagation()
. The passive modifier enhances scrolling performance on touch and wheel events. Conversely, the nonpassive modifier explicitly sets passive: false
for the event listener. The capture modifier triggers the handler during the capture phase instead of the bubbling phase. Using the once modifier removes the event handler after it has been triggered once. The self modifier ensures that the handler is activated only if the e.target
corresponds to the element itself. Lastly, the trusted modifier only activates the handler if e.isTrusted
is true, indicating that the event was initiated by a user action rather than programmatically. Chaining these modifiers allows for a more sophisticated approach to event handling, enabling greater precision and control.
<button
on:click|once|capture={() => console.log('Hello, world')}
>
Click
</button>
Example of DOM event modifier chaining
Component Dispatched Events
Components can communicate with each other by dispatching custom events. This decoupled approach allows for flexible communication and interaction between components. To dispatch an event, you can use the createEventDispatcher function provided by Svelte. This function returns a dispatch function that you can use to send custom events with optional data payloads.
On the receiving end, the on:event-name
directive is used to listen for and handle these custom events. This enables you to respond to events and access any data passed along with them using e.detail
. By leveraging custom events, Svelte components can effectively communicate and coordinate their actions.
In contrast to DOM events, event bubbling, which involves the propagation of events from innermost child components to outermost ones, does not occur automatically with component events.
If you wish to listen to an event on a deeply nested component, the intermediate components need to forward the event to the parent component. This process, known as event forwarding, enables event handling at various levels within the component hierarchy. It is worth noting that event forwarding is also applicable to DOM events.
The example above showcases component event dispatching, event forwarding, and event handling:
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
// Dispatch a "data" event
const handleClick = () => {
dispatch('data', { message: 'Hello from Inner' });
}
</script>
<button on:click={handleClick}>Click</button>
Inner.svelte
<script>
import Inner from './Inner.svelte';
</script>
<!-- Forward component dispatched event instead of handling it -->
<Inner on:data />
Outer.svelte
<script>
import Inner from './Inner.svelte';
import Outer from './Outer.svelte';
</script>
<!-- Handle component dispatched events -->
<Inner
on:data={e => console.log('Inner:', e.detail.message)}
/>
<Outer
on:data={e => console.log('Outer:', e.detail.message)}
/>
ComponentDispatcedEvents.svelte
The example above illustrates DOM event forwarding and DOM event handling:
<!-- Forward click event -->
<button on:click>Click</button>
ForwardButton.svelte
<!-- Handle click event -->
<button
on:click={() => console.log('Handled click event')}
>
Click
</button>
HandleButton.svelte
<script>
import ForwardButton from './ForwardButton.svelte';
import HandleButton from './HandleButton.svelte';
let text = '';
</script>
<div class='container'>
<div>
<!-- Handling DOM event -->
<input on:input={ ({ target }) => text = target.value} />
<p>Input: {text}</p>
</div>
<div>
<!-- Handle forwarded click event -->
<ForwardButton
on:click={() => console.log('Received click event')}
/>
<!--
Event is not forwarded, hence the
click handler will not be triggered
-->
<HandleButton
on:click={() => console.log('Received click event')}
/>
</div>
</div>
<style>
div {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1em;
}
.container {
flex-direction: row;
gap: 5em;
}
</style>
DOMEvents.svelte
Component Lifecycle
The lifecycle of a component refers to the various stages it goes through, from creation to destruction, with several functions available to execute code at crucial moments.
The onMount function triggers after the component has been rendered to the DOM, allowing actions like fetching data from an API or setting up event listeners once the component has been mounted. Additionally, you can return a callback function that executes when the component when the component is about to be destroyed. This callback function will be executed within the onMount callback's function scope, which is useful for accessing variables.
The onDestroy function offers a cleaner semantic approach compared to the callback within onMount, as it explicitly signifies the purpose of performing clean-up tasks before the component is removed from the DOM. This function can be utilized when direct access to the onMount scope is not required, offering a structured approach to managing clean-up operations.
The beforeUpdate and afterUpdate functions allow you to perform imperative actions before and after the DOM is updated to reflect changes in the component's data. It is important to note that beforeUpdate runs initially before onMount, while afterUpdate runs after onMount.
Unlike the other functions, the tick function can be called at any time. Its purpose is to pause code execution. When invoked, it returns a promise that resolves once any pending state changes have been applied to the DOM. This functionality ensures that specific DOM updates have taken place before further code execution proceeds.
<script>
import { onMount, onDestroy } from 'svelte';
export let name;
onMount(() => console.log('Item created'));
onDestroy(() => console.log('Item destroyed'));
</script>
<li>{name}</li>
Item.svelte
<script>
import { onMount, beforeUpdate, afterUpdate } from 'svelte';
import Item from './Item.svelte';
let show = true;
let items = [];
// Simulate an API call
function fetchData() {
const data = Array
.from({ length: 5 })
.map((_, idx) => `Item ${idx + 1}`);
return new Promise(resolve => {
setTimeout(
() => resolve({ data }),
2000
);
});
}
onMount(async () => {
console.log('List is being created');
const res = await fetchData();
items = res.data;
});
beforeUpdate(() => console.log('List is about to be updated'));
afterUpdate(() => console.log('List has been updated'));
</script>
<button on:click={() => show = !show}>
Toggle
</button>
{#if show}
<ul>
{#each items as name}
<Item {name} />
{/each}
</ul>
{:else}
<p>Fetching data...</p>
{/if}
List.svelte
The code example above includes a List component that manages a list of items. When the List component is mounted, it initiates a simulated API call using the fetchData function. This data is then used to create instances of the Item component.
The List component includes beforeUpdate and afterUpdate functions that log messages before and after each update. The visibility of each Item component is controlled by a show variable, toggled by a button click. If show is true, the Item components are displayed, otherwise, a message "Fetching data…" is shown.
The Item component logs messages upon mounting and destruction. Toggling visibility causes the Item component to be destroyed and recreated, triggering the beforeUpdate and afterUpdate functions of the List component during re-rendering.
<script>
import { tick } from 'svelte';
let count = 0;
let p;
async function increment() {
count++;
console.log('Before tick:', p.textContent);
await tick();
console.log('After tick:', p.textContent);
}
</script>
<p bind:this={p}>{count}</p>
<button on:click={increment}>
Increment
</button>
Tick.svelte
In the code example above, there is a
p
element that is bound to the variable p. The initial value of count is 0. When the Increment button is clicked, the increment function is called.Within the function, count is incremented by 1, and the current value of
p.textContent
is logged to the console.Initially, before the
await tick()
statement, the value ofp.textContent
is logged as 0. This is because the DOM update to reflect the increment in count is not immediate.By utilizing the
await tick()
statement, you allow the DOM to update and apply the state change before continuing with the code execution. Once the promise returned by tick resolves, the code execution proceeds, and the updated value ofp.textContent
, which is now 1, is logged to the console.
Stores
Stores in Svelte are objects designed to hold specific values and provide a subscribe method for interested parties to receive notifications of any changes. They offer an efficient and reactive way to manage and share state among components. By using stores, you can store and update values that are accessible and modifiable by different components, even if they are unrelated. When the value within a store changes, any subscribed component will automatically update to reflect the new value.
To create a store, you can use the writable function to create a store that allows both reading and writing, or the readable function to create a store that can only be read from. Writable stores can be updated or set by subscribers, while readable stores do not have set and update methods, allowing subscribers only to read the data.
Derived stores, created with the derived function, are readable stores computed or derived from existing stores. They enable you to create new stores that automatically update whenever the values of the dependent stores change.
To reference the value of a store, you can prefix the store name with a $
. For example, $count
references the value of a store called count
. This shorthand notation allows you to access the value of the store in components or expressions.
The svelte/store
module provides the writable, readable, and derived functions. Additionally, it provides the get function, which fetches the value of a store you are not subscribed to, and the readonly function, which produces a new readable store from an existing store. Note that the get function is implemented by setting up a subscription, reading the value, and then unsubscribing, which is generally discouraged.
In the context of reactivity, subscribing refers to the process of registering an interest in modifications to a specific data item or state.
When you subscribe to a reactive entity, such as a variable or a store, you are essentially indicating that you want to be notified whenever that entity's value changes.
import { writable, readable, derived, readonly } from 'svelte/store';
/**
* Writable store.
* Allows to retrieve and update the data.
*/
export const count = writable(0);
/**
* Readable store created with readonly().
* Allows to retrieve the data.
*/
export const readableCount = readonly(count);
/** Derived readble store.
* Allows to retrieve the computed data.
*/
export const doubledCount = derived(count, (cnt) => 2 * cnt);
/**
* Derived readble store from multiple stores.
* Allows to retrieve the computed data.
*/
export const multiCount = derived(
[count, readableCount],
vals => vals[0] + vals[1],
);
/**
* Readable store.
* Allows to retrieve the data.
*/
export const random = readable(randNum(), start);
// Generate a random number from 0 to 99
function randNum() {
return Math.floor(Math.random() * 100);
}
// Start function to generate values
function start(set) {
// Generate and set a random number every second
const intervalId = setInterval(() => set(randNum()), 1000);
/**
* Stop function will be called on component destruction.
* If multiple components use the random store at the same time,
* and one of them calls the stop function, it will impact all.
*/
return () => clearInterval(intervalId);
}
stores.js
<script>
import { onDestroy } from 'svelte';
import {
count,
readableCount,
doubledCount,
multiCount,
random
} from './stores';
/**
* Store values received from subscriptions
* will be stored to these variables.
*/
let countValue, doubledCountValue, randomValue;
/**
* Subscribe to the stores and
* store the returned unsubscribe functions.
*/
const unsubscribeCount = count
.subscribe(cnt => countValue = cnt);
const unsubscribeDoubledCount = doubledCount
.subscribe(val => { doubledCountValue = val; });
const unsubscribeRandom = random
.subscribe(val => randomValue = val);
// Unsubscribe when component is about to be removed from the DOM
onDestroy(() => {
unsubscribeCount();
unsubscribeDoubledCount();
unsubscribeRandom();
});
</script>
<p>Count: {countValue}</p>
<p>Readable Count: {$readableCount}</p>
<p>Doubled Count (1 Store): {doubledCountValue}</p>
<p>Doubled Count (2 Stores): {$multiCount}</p>
<p>Random Number: {randomValue}</p>
<!--
Writable stores can be updated or set by the subscribers.
Readable stores do not have set and update methods.
-->
<button on:click={() => count.update(cnt => cnt + 1)}>
Increment Count
</button>
<button on:click={() => count.set(0)}>
Reset Count
</button>
Numbers.svelte
In the code above, the stores.js file introduces various types of stores. It begins by creating a writable store named
count
with an initial value of 0, followed by a readable store,readableCount
, generated using the readonly function.Additionally, a derived store named
doubledCount
is established, computing its value as twice thecount
store's value. Another derived store,multiCount
, calculates its value as the sum ofcount
andreadableCount
.The code also includes a readable store named
random
, which generates random numbers from 0 to 99 and updates every second through the start function. This function orchestrates the continuous generation of random numbers and clears the interval when necessary, typically upon component destruction.The Numbers component subscribes to the
count
,doubledCount
, andrandom
stores, storing their values in variables countValue, doubledCountValue, and randomValue, respectively.The unsubscribe functions are stored for cleanup purposes. During component destruction, the onDestroy lifecycle function is employed to unsubscribe from each store.
The values of the stores are displayed in the markup, with the use of
$
to access the values ofreadableCount
andmultiCount
stores. Additionally, buttons are included for users to update the writable storecount
by incrementing its value or resetting it to 0, triggering updates in all related stores.This code effectively showcases how stores can be utilized for state management and interaction within a component.
Custom Stores
By implementing the subscribe method, developers can easily create custom stores with domain-specific logic. This flexibility empowers developers to craft their own stores tailored to meet specific requirements. Custom stores offer a high level of control and encapsulation for effective state management.
For example, a custom store designed for a user's shopping cart, named cart, can be generated using a createCart function. The cart store maintains an array of items that represent the user's shopping cart. It offers methods such as addToCart, removeFromCart, and emptyCart to alter the cart state. By subscribing to the cart store, other components can effortlessly receive updates whenever changes occur in the cart, enabling them to reflect these modifications in their rendering.
import { writable } from 'svelte/store';
// Create a custom store for a shopping cart
function createCart() {
const { subscribe, set, update } = writable([]);
// Do not expose set and update methods
return {
subscribe,
addToCart: item => update((cart) => [...cart, item]),
removeFromCart: itemId => {
update(cart =>
cart.filter((item) => item.id !== itemId)
);
},
emptyCart: () => set([]),
};
}
// Singleton cart
export const cart = createCart();
cart.js
<script>
import { onDestroy } from 'svelte';
import { cart } from './cart';
let items;
let id = 0;
const unsubscribe = cart.subscribe(val => items = val);
onDestroy(unsubscribe);
function addToCart() {
cart.addToCart({ id, name: `Item ${id}` });
id++;
}
function removeFromCart() {
cart.removeFromCart(--id);
}
function emptyCart() {
cart.emptyCart();
id = 0;
}
</script>
<h3>Cart Summary</h3>
<ul>
{#each items as { id, name } (id)}
<li>{name}</li>
{/each}
</ul>
<button on:click={addToCart}>Add Item</button>
<button on:click={removeFromCart}>Remove Last Item</button>
<button on:click={emptyCart}>Empty Cart</button>
ShoppingCart.svelte
In this example, the createCart function returns an object with a subscribe method, allowing components to subscribe to changes in the cart state. It also exposes 3 methods to update the cart state: addToCart, removeFromCart, and emptyCart.
The addToCart method adds an item to the cart by updating the cart state with a new array that includes the item.
Similarly, the removeFromCart method removes an item from the cart by updating the cart state with a new array that excludes the item.
Lastly, the emptyCart method resets the cart state to an empty array.
To ensure a clean and concise interface, the update and set methods are not exposed. This simplifies the usage of the cart store and allows components to interact with the cart state using the provided methods.
In the ShoppingCart component, a variable id is initialized to 0, and a variable items is declared to hold the cart items. The unsubscribe variable is assigned the result of subscribing to the cart store. The onDestroy function is used to unsubscribe from the cart store when the component is destroyed, ensuring that there are no memory leaks.
The addToCart function adds an item to the cart by calling the addToCart method of the cart store and passing an object with the id and name properties. The id is incremented after each addition.
The removeFromCart function removes the last added item from the cart by calling the removeFromCart method of the cart store and passing the current id value decremented by 1.
The emptyCart function resets the id to 0 and empties the cart by calling the emptyCart method of the cart store.
In the markup a list of items is displayed, along with 3 buttons for cart updates. When changes occur in the cart store, Svelte detects them and re-renders the component. Consequently, the list of items dynamically updates to reflect the latest state of the cart.
Slots
Slots allow you to transfer content from a parent component to its child component. This feature provides greater flexibility and dynamism when composing components.
Named slots are particularly handy when you need to pass different content to specific slots within a component. By assigning a name to a slot, you can determine which content should be displayed in that slot, giving you more control over the composition of your components.
In situations where a component does not receive any content for a specific slot, you can use slot fallbacks. By providing fallback content, you ensure that the component remains functional even if some slots are left empty.
Slot props allow you to pass data from the parent component to the content displayed within a slot. This empowers you to customize the content based on the data provided by the parent component, giving you more flexibility in designing your components.
Conditional slots allow to selectively render distinct sets of content within a component, depending on specific conditions. This powerful feature enables the dynamic rendering or omission of different parts of a component's template, providing flexibility and adaptability.
<div class='card'>
<header>
<slot name='name' />
</header>
<!--
There is no way to apply CSS directly to a slot,
wrap it in a DOM element.
i.e. ".card > *" will not match an unwrapped slot
inside the card container, so target the parent element.
-->
<div>
<slot />
</div>
<footer>
<span>Power:</span>
<slot name='power'>100</slot>
</footer>
</div>
<style>
.card {
width: 22em;
border: 2px solid #000;
border-radius: 0.5em;
background-color:slategray;
}
.card > * {
display: flex;
justify-content: center;
align-items: center;
width: auto;
padding: 1em;
margin: 0.5em;
background-color: cornflowerblue;
color: #fff;
font-size: 1.2rem;
}
header {
border-radius: 0.3em 0.3em 0 0;
}
footer {
border-radius: 0 0 0.3em 0.3em;
}
footer > span {
padding-right: 0.5em;
}
:is(header, footer > span) {
font-weight: 800;
}
:not(header, footer) {
text-align: center;
text-wrap: balance;
}
</style>
Card.svelte
<script>
import Card from './Card.svelte';
</script>
<Card>
<span>
A strong and valiant <strong>fighter</strong>
with lightning attacks,
defensive skills, and empowering abilities,
inspiring allies and intimidating foes.
</span>
<span slot='name'>Mighty Warrior</span>
<span slot='power'>245</span>
</Card>
Slots.svelte
In the code above, the Card component utilizes slots to allow for customization and injection of content. It offers 2 named slots, namely name and power. Any content assigned to these slots will be displayed in their respective places within the Card component.
The first span element is not assigned to any specific slot and will be placed in the default slot, represented by
<slot />
without a name.The second span element, designated with
slot='name'
, will be assigned to the name slot.Similarly, the third span element, marked with
slot='power'
, will be assigned to the power slot. If no content is provided for the power slot, the fallback content 100 will be shown.It is important to note that you cannot apply CSS styles directly to a slot. To style the slot content, you must wrap the slot inside an element and apply styles to that wrapping element.
<script>
let hovering;
const enter = () => hovering = true;
const leave = () => hovering = false;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:mouseenter={enter} on:mouseleave={leave}>
<slot {hovering} />
</div>
<style>
div {
width: 10em;
border: 2px solid #000;
text-align: center;
}
</style>
Hoverable.svelte
<script>
import Hoverable from './Hoverable.svelte';
</script>
<Hoverable let:hovering={active}>
{#if active}
<p>I'm being hovered upon</p>
{:else}
<p>Hover over me</p>
{/if}
</Hoverable>
SlotProps.svelte
The Hoverable component facilitates tracking if an element is being hovered over and relaying this to its slotted content. It encloses the slotted content with a div element that monitors mouseenter and mouseleave events.
On mouse entry, the enter function triggers, setting the hovering variable to
true
.On mouse exit, the leave function triggers, setting hovering to
false
.In the slotted content, the
let:hovering={active}
syntax receives the hovering value from Hoverable. The active variable is utilized for conditional content rendering, exclusively within the slot. It is worth noting that you can simplify by usinglet:hovering
and checking if hovering is true, eliminating the need to define active separately.
<div>
<p>
<span>Name:</span>
<slot name='name' />
</p>
{#if $$slots.email}
<p>
<span>Email:</span>
<slot name='email' />
</p>
{/if}
{#if $$slots.phone}
<p>
<span>Phone:</span>
<slot name='phone' />
</p>
{/if}
</div>
<style>
div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 10em;
border: 2px solid #000;
padding: 0.5em;
}
</style>
Profile.svelte
<script>
import Profile from './Profile.svelte';
</script>
<div>
<Profile>
<span slot='name'>John Doe</span>
<span slot='email'>john@mail.com</span>
</Profile>
<Profile>
<span slot='name'>George Doe</span>
<span slot='phone'>+1234567890</span>
</Profile>
<Profile>
<span slot='name'>Andrew Doe</span>
</Profile>
</div>
<style>
div {
display: flex;
justify-content: center;
align-items: center;
gap: 5em;
}
</style>
Users.svelte
In this code example, the
$$slots
special variable is employed to access defined slots, enabling conditional content rendering based on specific slot presence. Within{#if}
blocks,$$slots
is used to verify the existence of certain slots in the component. When a slot with a particular name is present, the associated section is rendered, facilitating dynamic content rendering according to the slots supplied by the parent component.
Context API
The Context API in Svelte offers a convenient feature that simplifies the sharing of data and state between components, eliminating the need for prop drilling. Prop drilling refers to passing data through multiple intermediary components. However, with the Context API, a parent component can directly pass data to any children components, even if they are not immediate descendants.
The context is an object that acts as a container for shared data and state. It can hold various types of data, including primitive values, arrays, objects, and stores. Unlike other frameworks, the key used in the Context object can be any value, even non-strings, giving you more control over access to the Context.
It is important to keep in mind that the Context API in Svelte is not reactive by default. This means that the value of the Context is set once when the component is mounted and does not automatically update when the value changes. However, you can make use of stores within the Context object to pass dynamic values to child components. This allows the child components to reactively update when the store values change. This capability provides a powerful way to manage and share state across components in a reactive manner.
Note that the context object itself is the specific object that holds the shared data, while the Context API is the feature that enables the creation and usage of this context object within the Svelte framework.
<script>
import { getAllContexts, getContext, hasContext } from 'svelte';
const name = hasContext('name') ? getContext('name') : '';
const { format } = getContext('format');
const contextMap = getAllContexts();
</script>
<p>{format(name)}</p>
<p>Contexts: {Array.from(contextMap.keys()).join(', ')}</p>
Child.svelte
<script>
import Child from './Child.svelte';
</script>
<Child />
Parent.svelte
<script>
import Parent from './Parent.svelte';
</script>
<Parent />
Grandparent.svelte
<script>
import { setContext } from 'svelte';
import Grandparent from './Grandparent.svelte';
setContext('name', 'Jonh Doe');
setContext('format', {
format: name => `Hello, ${name}`
});
</script>
<Grandparent />
Context.svelte
In the code above, the Context component renders the Grandparent component, which then renders the Parent component, and subsequently, the Parent component renders the Child component, establishing a component hierarchy.
Within the Context component, the setContext function is used to define the context data. The name key is associated with the value "Jonh Doe", while the format key is connected to an object containing a format function.
The Child component retrieves and utilizes the context data, showcasing the seamless passing of information through various component levels.
<script>
import { getContext, setContext } from 'svelte';
import { writable } from 'svelte/store';
let name = writable('');
setContext('name', name);
let message = getContext('name');
function randomName() {
const names = ['George', 'John', 'Peter', 'Dennis'];
const idx = Math.floor(Math.random() * names.length);
return names[idx];
}
</script>
<p>Hello, {$message}</p>
<button on:click={() => $name = randomName()}>
Change Name
</button>
ReactiveContext.svelte
This code example illustrates a reactive context implementation. It establishes a dynamic data flow by leveraging context and store functionalities.
Initially, a writable store named name is created and assigned as the context data. The message variable retrieves and stores this context data. A randomName function is defined to generate a random name from a predefined list.
Within the markup, a p element displays the message variable, which holds a writable store as its value. Furthermore, a button triggers the randomName function to update the name store, leading to automatic updates in the displayed message content.
Special Elements
Svelte provides special elements that enhance component markup.
The <svelte:self>
refers to the current component and is commonly used for recursive components or self-referencing within a template.
The <svelte:component>
and <svelte:element>
elements enable dynamic switching between different components or DOM elements based on specific conditions. For instance, <svelte:component>
can render a login form when the user is not authenticated and switch to a user profile once authenticated. Similarly, <svelte:element>
dynamically renders HTML elements based on user interactions or conditions.
The <svelte:head>
permits direct manipulation of the head element in components. It proves useful for dynamically modifying or adding elements like title, style or meta tags.
The <svelte:window>
, <svelte:body>
, and <svelte:document>
elements allow event listening on window, body, and document respectively within components. These elements facilitate handling global events occurring outside the component's immediate scope, responding to user interactions or external changes.
The <svelte:fragment>
element allows content placement in a named slot without requiring a container DOM element. It proves useful when you want to avoid unnecessary wrapping elements.
Lastly, with <svelte:options>
, compiler options can be configured. This customization allows you to tailor the behavior of your components by enabling or disabling specific features or optimizations.
export let data = [
{
name: "dir1",
files: [
{
name: "dir1A",
files: [{ name: "file.json" }],
},
],
},
{
name: "dir2",
files: [{ name: "file.txt" }],
},
{ name: "file.txt" },
];
data.js
<script>
export let name;
</script>
<span>{name}</span>
File.svelte
<script>
import File from './File.svelte';
export let name, files, expanded;
</script>
<button on:click={() => expanded = !expanded}>
/{name}
</button>
{#if expanded}
<div class='expanded'>
{#each files as file}
{@const isDir = file.files}
{#if isDir}
<svelte:self {...file} />
{:else}
<File {...file} />
{/if}
{/each}
</div>
{/if}
<style>
* {
display: block;
}
button {
background-color: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
}
.expanded {
margin-left: 1em;
}
</style>
Directory.svelte
<script>
import Directory from './Directory.svelte';
import { data } from './data';
</script>
<Directory name='root' files={data} expanded />
FileSystem.svelte
In the code above, the FileSystem component serves as the entry-point component where the Directory component is rendered and data are imported. Within the Directory component, the logic for displaying a directory is implemented, with props such as name, files, and expanded being passed. Toggling the expanded state controls the visibility of directory contents, iterating over the files array to render either the Directory or File component based on the item type.
The special element
<svelte:self {...file} />
is utilized within the Directory component to allow recursive rendering, enabling the component to render itself with different props based on the data in the files object. Meanwhile, the File component focuses on displaying the name of a file received as a prop. Utilizing this special element, simplifies the component's structure and makes the code more readable and maintainable.
<span>Admin</span>
<style>
span {
background-color: red;
color: gold;
font-weight: 600;
height: max-content;
padding: 0.2em 1em;
margin: 0.5em;
border-radius: 0.5em;
}
</style>
AdminBadge.svelte
<span>Moderator</span>
<style>
span {
background-color: green;
color: #fff;
font-weight: 500;
height: max-content;
padding: 0.2em 1em;
margin: 0.5em;
border-radius: 0.5em;
}
</style>
ModeratorBadge.svelte
<span>User</span>
<style>
span {
background-color: gray;
color: #fff;
font-weight: 500;
height: max-content;
padding: 0.2em 1em;
margin: 0.5em;
border-radius: 0.5em;
}
</style>
UserBadge.svelte
<script>
import AdminBadge from './badges/AdminBadge.svelte';
import ModeratorBadge from './badges/ModeratorBadge.svelte';
import UserBadge from './badges/UserBadge.svelte';
const badges = [
{ name: 'user', component: UserBadge },
{ name: 'moderator', component: ModeratorBadge },
{ name: 'admin', component: AdminBadge }
];
const formats = ['h4', 'h5', 'h6', 'p'];
let selectedBadge = badges[0];
let selectedFormat = formats[0];
</script>
<div>
<svelte:component this={selectedBadge.component} />
<svelte:element this={selectedFormat}>
Username
</svelte:element>
</div>
<div>
<!-- Select component -->
<select bind:value={selectedBadge}>
{#each badges as badge}
<option value={badge}>{badge.name}</option>
{/each}
</select>
<!-- Select element -->
<select bind:value={selectedFormat}>
{#each formats as format}
<option value={format}>{format}</option>
{/each}
</select>
</div>
<style>
div {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
</style>
Profile.svelte
In the code above, a Profile component and 3 badge components are defined. The Profile component displays a badge, which indicates the user role, and a username in a text format. The badge type and text format can be chosen from dropdown menus.
The
<svelte:component>
and<svelte:element>
special elements are used to reflect the chosen options in real-time.Note that this simple example showcases the real-time switching between components. Typically, in real-world projects, you would not need 3 nearly identical components like in this demonstration.
<script>
let show, key, selection;
const externalCss = 'https://cdnjs.cloudflare.com' +
'/ajax/libs/nes.css/2.3.0/css/nes.min.css';
</script>
<svelte:head>
<link
rel='stylesheet'
href='https://fonts.googleapis.com/css?family=Gelasio'
/>
<link
rel='stylesheet'
href={externalCss}
/>
<title>Svelte App</title>
</svelte:head>
<svelte:window
on:keydown={e => key = e.key}
/>
<svelte:body
on:mouseenter={() => show = true}
on:mouseleave={() => show = false}
/>
<svelte:document
on:selectionchange={() => selection = `${document.getSelection()}`}
/>
<div>
{#if show}
<p>Pressed: {key || '--'}</p>
<p>Selected Text: {selection || '--'}</p>
{:else}
<p>Hover over here</p>
{/if}
</div>
<style>
div {
font-family: Gelasio;
border: 2px solid #000;
margin: 1em;
padding: 1em;
}
</style>
App.svelte
In this code example, the special elements
<svelte:head>
,<svelte:window />
,<svelte:body />
, and<svelte:document />
are employed to interact with different parts of the browser environment.The
<svelte:head>
element manipulates the document's head by adding external stylesheets, external fonts, and setting the title.The
<svelte:window />
element listens for keydown events on the window and updates the key variable accordingly.
The <svelte:body />
element handles mouseenter and mouseleave events to control the visibility of content based on the show variable.Lastly, the
<svelte:document />
element captures selectionchange events on the document, updating the selection variable with the currently selected text.
<div>
<slot name='header'>No header provided</slot>
<p>Content</p>
<slot name='footer' />
</div>
Widget.svelte
<script>
import Widget from './Widget.svelte';
</script>
<Widget>
<!-- "slot='header'" is visible on the source code -->
<h3 slot='header'>Header</h3>
<!-- "slot='footer'" is not visible on the source code -->
<svelte:fragment slot='footer'>
<h4>Footer</h4>
</svelte:fragment>
</Widget>
Fragment.svelte
In the code example above, the Fragment component renders a Widget component.
Within the Widget component, slots are designated for header and footer content. The header slot is populated with an h3 element displaying the text "Header". Conversely, the footer slot contains an h4 element with the text "Footer" but is not immediately visible in the source code due to its encapsulation within a
<svelte:fragment />
element.
<!-- Only one per component allowed -->
<svelte:options
immutable
accessors={false}
/>
<p>Component</p>
Options.svelte
In the this code example, the
<svelte:options>
element is used to specify compiler options specific to a component. For instance, setting immutable, which is shorthand forimmutable={true}
, indicates that only immutable data is used for direct value change detection through referential equality checks.On the other hand, the default setting of
immutable={false}
means that Svelte takes a cautious approach when assessing changes to mutable objects.Additionally, enabling
accessors={true}
introduces getters and setters for the component's properties, whileaccessors={false}
is the default setting for component accessors.
Higher-order Components
A Higher-order Component (HOC) is a component that takes another component as an input and returns a new component with enhanced functionality. This pattern allows for code reusability and composability.
In Svelte, this concept differs from traditional frameworks. Unlike frameworks where HOCs enhance component functionality, Svelte utilizes slots for component composition. Additionally, Svelte introduces the <svelte:component>
special element for dynamic component rendering based on component definitions. Despite the absence of HOCs in Svelte, similar functionality can be achieved through these alternative mechanisms.
Module Context
A module-level script in a component allows for defining variables, functions, and importing external modules that can be shared across all instances of the component. A module-level script is created within the component using the script tag with a context='module'
attribute. This script runs only once during the component's compilation and is not re-evaluated for each instance. It is beneficial for sharing data or utility functions across multiple instances of the component, such as for API calls. It is worth noting that a module-level script operates in a separate module context from the component's instance-level script.
Anything exported from the module-level script becomes an export from the module itself. The module-level script is executed before the instance-level script, making its variables or functions readily available for use within the instance-level script.
Although the module-level script does not directly access the instance-level script, the context API can be utilized to pass essential information from the instance-level script to the module-level script.
export async function fetchData() {
const data = [1, 2, 3, 4, 5];
return new Promise((resolve) => {
setTimeout(() => resolve(data), 2000);
});
}
api.js
<script context='module'>
import { getContext, setContext } from 'svelte';
import { fetchData } from './api';
const data = await fetchData();
console.log('I will be printed only once');
export function sharedFunction() {
return `Hello, ${getContext('name')}`;
}
</script>
<script>
export let name;
setContext('name', name);
console.log(`Item: ${sharedFunction()}`);
</script>
{#each data as num}
<p>{num}</p>
{/each}
Item.svelte
<script>
import Item from './Item.svelte';
</script>
<Item name='A' />
<Item name='B' />
<Item name='C' />
List.svelte
In the code example above, the List component displays 3 instances of the Item component.
Each Item component comprises 2 script sections. The module-level script section, labeled with
context='module'
, imports the fetchData function fromapi.js
and retrieves data during the component's importation. The fetchData function returns a Promise that eventually resolves to an array of numbers after a brief delay.This module-level script runs only once, upon the initial rendering of the Item component. Additionally, the message "I will be printed only once" is logged to the console.
Furthermore, a sharedFunction is defined. This function accesses the context object to retrieve the value of the name prop.
In the instance-level script section, the name context is established, and the sharedFunction is called.
Transitions & Animations
Svelte provides several modules that offer different functionalities for creating dynamic and animated experiences. One of these modules is svelte/motion
. It exports 2 functions, tweened and spring. These functions enable the creation of writable stores, which smoothly transition between values over a specified duration when updated or set. This allows for a more visually pleasing and gradual change in the variables.
Another module, svelte/transition
, exports 7 functions, fade, blur, fly, slide, scale, draw, and crossfade. These functions are specifically designed to be used with transitions. Transitions are visual effects that can be applied to elements, such as fading in or out, sliding in from a direction, or scaling up or down. These functions provide ready-to-use transitions that can be easily applied to elements in your components.
To control the rate of change over time in these transitions and animations, Svelte provides easing functions. The svelte/easing
module contains 31 named exports, including a linear ease and 3 variants, in, out, and inOut, of 10 different easing functions. Easing functions define how the transition progresses, allowing you to create smooth and natural-looking animations.
Lastly, the svelte/animate
module exports a single function that is used for Svelte animations. Animations are more complex than transitions and allow for more advanced effects and interactions.
Motion
Transitions play a crucial role in animating changes within the DOM elements when data or component state changes. Tweens and springs are motion techniques used to create smooth and dynamic transitions between different states of a component.
Tweens, short for tweening or in-betweening, refer to the process of generating intermediate frames between 2 keyframes to create the illusion of smooth animation. They smoothly interpolate between 2 states over a specified duration and are ideal for creating linear or easing animations, such as fading in/out, sliding, or scaling elements. You can use the tweened function to create a tweened value that transitions smoothly from one state to another. Tweens offer options like duration, delay, easing function, and more.
Springs, on the other hand, provide a more physics-based approach to animation. They simulate the behavior of a physical spring, resulting in a more natural and livelier feel to the animation. Springs are commonly used for effects like bouncing, overshooting, or elastic animations. You can adjust parameters such as stiffness, damping, and mass to control the behavior of the spring animation. Svelte provides the spring function for creating spring-based animations.
The choice between tweens and springs depends on the desired effect and the specific behavior you wish to achieve. Both techniques offer customization options to tailor your animations.
<script>
import { cubicOut } from 'svelte/easing';
import { tweened } from 'svelte/motion';
const initialValue = 0;
const step = 0.25;
// Options for the tweened animation
const options = {
delay: 0, // Time in ms before the tween starts
duration: 400, // Time in ms or (from, to) => time in ms
easing: cubicOut
};
let progress = initialValue;
const tweenProgress = tweened(initialValue, options);
function increase() {
if (progress !== 1) {
progress += step;
}
if ($tweenProgress < 1) {
// Returns a promise that resolves when it completes
tweenProgress.update(n => n + step);
}
}
function reset() {
progress = 0;
// Returns a promise that resolves when it completes
tweenProgress.set(initialValue);
}
</script>
<div>
<p>Without tweening:</p>
<progress value={progress} />
</div>
<div>
<p>Tweened:</p>
<progress value={$tweenProgress} />
</div>
<button on:click={increase}>Increase</button>
<button on:click={reset}>Reset</button>
<style>
div {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 1em;
margin-bottom: 1em;
}
p {
width: 8em;
}
progress {
flex: 1;
}
</style>
TweenProgressBar.svelte
In the this code snippet, a tweened animation is implemented using the tweened method. The progress variable stores the current value of the animation, which is smoothly updated over time using the update method of the tweened store. Initially set to 0, this value serves as the starting point of the animation, with customizable aspects like delay, duration, and easing function controlled by the options object.
The increase function responds to the Increase button click by incrementing the progress value by 0.25, ensuring it does not exceed 1.
By binding the progress value to the value attribute of the
progress
element, the progress bar visually reflects the animation's state, smoothly transitioning as the value evolves. This setup allows for a gradual and controlled animation effect.Additionally, the presence of a
progress
element without tweening provides a direct comparison with the tweened animation, showcasing the difference in visual presentation and smoothness between the 2 approaches.
<script>
import { tweened } from 'svelte/motion';
const initialValue = 100;
const step = 50;
const options = {
delay: 0, // Time in ms before the tween starts
duration: 400, // Time in ms or (from, to) => time in ms
easing: t => t, // Linear easing
interpolate: (from, to) => t => from + (to - from) * t
};
const tween = tweened(initialValue, options);
function increase() {
tween.update(n => n + step);
}
function reset() {
tween.set(initialValue);
}
</script>
<button on:click={increase}>Increase</button>
<button on:click={reset}>Reset</button>
<div>
<p style:--size={$tween + 'px'}>
{$tween} x {$tween}
</p>
</div>
<style>
div {
display: flex;
margin: 2em;
}
p {
width: var(--size);
height: var(--size);
padding: 2em;
background-color: red;
color: #fff;
}
</style>
TweenInterpolationSquare.svelte
In this code example, the tween variable holds the current value of the animation, which is continuously updated over time through the update method of the tweened store. Initially set at 100, this value serves as the starting point for the animation. The options object contains a custom interpolation function that defines how values between the initial and final points are computed throughout the animation. By using this interpolation function, you can precisely manage the transition of values between the start and end points, providing flexibility in defining the animation's behavior.
When the Increase button is clicked, the increase function is activated, increasing the tween value by 50. As a result, the width and height of the p element dynamically adjust based on the updated tween value, visually representing the changing state of the animation.
It is essential to understand that the easing function and the interpolation function have distinct roles in animations.
The easing function controls the timing and pace of the animation, determining how the intermediate values progress between the starting and ending points. Easing functions can create various effects such as linear motion, smooth acceleration, or bouncing movements, influencing the speed and smoothness of the animation to shape the perception of motion.
In this specific case, a linear easing function is utilized to ensure a consistent speed of animation progression.
On the other hand, the interpolation function calculates the actual values between the start and end points during the animation. By taking the initial value from and the target value to as inputs, it generates the interpolated value based on a specified parameter t that represents the animation's progress. This function plays a pivotal role in determining how values transition between the start and end points, contributing to the overall fluidity and visual appeal of the animation.
The custom interpolation function employs linear interpolation, meaning the intermediate values are calculated by linearly scaling between the start and end values.
To summarize, the easing function manages the timing and perception of motion, while the interpolation function decides the actual values between the start and end points throughout the animation.
<script>
import { spring } from 'svelte/motion';
const initialValue = 100;
const step = 50;
const options = {
stiffiness: 0.5,
damping: 0.1
};
const size = spring(initialValue, options);
function increase() {
size.update(n => n + step);
}
function reset() {
size.set(initialValue);
}
</script>
<button on:click={increase}>Increase</button>
<button on:click={reset}>Reset</button>
<div>
<p style:--size={$size + 'px'}>
{$size} x {$size}
</p>
</div>
<style>
div {
display: flex;
margin: 2em;
}
p {
width: var(--size);
height: var(--size);
padding: 2em;
background-color: red;
color: #fff;
}
</style>
SpringSquare.svelte
In this code example, the animation is achieved using the spring function instead of custom interpolation and easing functions as in the previous code. The size variable is defined using the spring function with specific stiffness and damping options.
Stiffness affects the rigidity of the spring animation, while damping controls how quickly the animation settles. Higher stiffness results in quicker changes, while lower stiffness allows for smoother transitions.
Damping determines how rapidly the animation comes to a stop. A higher damping value leads to a faster settling animation with fewer oscillations, while a lower damping value allows for more pronounced movements before stability is reached, creating a more dynamic animation.
When the Increase button is clicked, the size value is updated by adding the step value of 50 through the increase function. This modification dynamically adjusts the width and height of the p element based on the updated size value.
In summary, this code creates animation effects that mimic a spring-like behavior, offering a different approach compared to manually setting interpolation and easing functions in the previous example.
Transition Application
Svelte provides a transition: directive that enables the integration of animations and transitions into elements when they are added, removed, or updated within the DOM. This directive empowers you to specify how an element should transition in and out of the DOM. You have a range of options available to tailor the animation, including parameters like duration, easing, delay, and more.
The in: and out: properties of the transition directive are specifically utilized to define the animation to be applied when an element is added or removed from the DOM.
Transitions initiated through the transition: directive are reversible. This feature ensures that if the element undergoing a transition requires re-transitioning in the opposite direction, the new transition will start from its current position rather than the initial or final points. This capability allows for a smooth adjustment of the transition based on user interactions, enhancing the overall fluidity of the animation process.
Transition Application
<script>
import { fade, fly } from 'svelte/transition';
let visible1 = true;
let visible2 = true;
let visible3 = true;
</script>
<div>
<div>
<label>
<input type='checkbox' bind:checked={visible1} />
Make Visible
</label>
{#if visible1}
<p transition:fade>
Fades in and out
</p>
{/if}
</div>
<div>
<label>
<input type='checkbox' bind:checked={visible2} />
Make Visible
</label>
{#if visible2}
<!--
"transition" directive with parameters.
Transition is reversible,
meaning if you toggle the checkbox while
the transition is ongoing,
it transitions from the current point.
-->
<p transition:fly={{ y: 200, duration: 2000 }}>
Flies in and out
</p>
{/if}
</div>
<div>
<label>
<input type='checkbox' bind:checked={visible3} />
Make Visible
</label>
{#if visible3}
<!--
Different transitions for when the element
is added and removed from the DOM.
-->
<p in:fly={{ y: 200, duration: 2000 }} out:fade>
Flies in and fades out
</p>
{/if}
</div>
</div>
<style>
div {
display: flex;
justify-content: center;
align-items: center;
gap: 1em;
}
div > div {
flex-direction: column;
width: 10em;
height: 5em;
padding: 1em;
border: 2px solid #000;
}
</style>
TransitionDirectives.svelte
In the code example above, 3 boolean variables: visible1, visible2, visible3, are declared and set to true. Each variable is bound to a checkbox input, allowing to toggle the visibility of a p element. For each variable, there is a section that is conditionally rendered based on the value of the variable.
The transitions fade and fly are utilized to animate the appearance and disappearance of elements when the checkboxes are toggled. When the transition: directive is used, the same transition is applied when the element enters or exits the DOM.
Alternatively, the in: and out: directives allow for different transitions to be applied in these scenarios. The fade transition smoothly fades the element in and out, while the fly transition moves the element vertically by 200 pixels over a duration of 2000 milliseconds.
Transition Events
Svelte provides transition events that enable you to monitor specific moments during the transition process, particularly when an element is being added or removed from the DOM. These events can be leveraged to trigger custom logic or execute actions based on the state of the transition.
<script>
import { fly } from 'svelte/transition';
let visible = true;
let status;
</script>
<p>Status: {status || '--'}</p>
<label>
<input type='checkbox' bind:checked={visible} />
Make Visible
</label>
{#if visible}
<p
transition:fly={{ y: 200, duration: 2000 }}
on:introstart={() => status = 'Intro started'}
on:outrostart={() => status = 'Outro started'}
on:introend={() => status = 'Intro ended'}
on:outroend={() => status = 'Outro ended'}
>
Flies in and out
</p>
{/if}
TransitionEvents.svelte
In the code example above, a boolean variable visible and a status variable are declared, with visible set to true. A p element displays the current status.
A checkbox is linked to the visible variable, allowing to toggle the visibility of the p element. Another p element is conditionally rendered based on the value of visible.When the second p element transitions in or out, event listeners are triggered to update the status variable with corresponding messages indicating the start and end of the intro and outro animations. This setup provides real-time feedback on the transition process.
Custom Transitions
Custom transitions for the transition:, in:, and out: directives can be specified using CSS or JavaScript.
CSS transitions involve defining animations through CSS properties such as transition-property, transition-duration, transition-timing-function, among others. These transitions are managed by the browser and offer a seamless way to animate CSS properties.
On the other hand, JavaScript transitions are animations created using JavaScript, providing more intricate control over the animation logic. They enable the development of complex animations beyond the capabilities of CSS transitions.
CSS transitions are commonly favored for their declarative nature, efficiency, and ease of implementation through the transition directive. However, there are scenarios where custom JavaScript transitions become essential. Custom JavaScript transitions are beneficial for intricate animations that cannot be achieved solely with CSS transitions. JavaScript provides enhanced control and flexibility to dynamically manipulate elements and craft sophisticated animations. When transitions rely on dynamic data or user interactions, custom JavaScript transitions are preferable as they allow for event responsiveness, real-time animation property updates, and condition-based transitions. Furthermore, for advanced or custom timing functions like elastic or bounce effects, JavaScript empowers you to define and integrate them into your animations effectively.
<script>
import { elasticOut } from 'svelte/easing';
let visible = true;
function fade(node, { delay = 0, duration = 400 }) {
const opacity = +getComputedStyle(node).opacity;
return {
delay,
duration,
css: t => `opacity: ${t * opacity};`
};
}
function spin(_node, { duration }) {
return {
duration,
css: t => {
const eased = elasticOut(t);
return `
transform:
scale(${eased})
rotate(${eased * 360}deg);
color: red;
`;
}
};
}
</script>
<label>
<input type='checkbox' bind:checked={visible} />
Make Visible
</label>
{#if visible}
<p
in:spin={{ duration: 8000 }}
out:fade
>
Spins in and fades out
</p>
{/if}
CSS.svelte
In this code example, there are 2 transition functions defined: fade and spin.
The fade function adjusts the opacity of an element, gradually fading it in or out over a specified duration. It calculates the opacity based on the current style of the element and returns an object with properties for delay, duration, and a css function to control the opacity.
The spin function applies a spinning effect to an element by scaling and rotating it based on the progress of the transition. It uses the elasticOut easing function to create a smooth animation effect.
The markup includes a checkbox labeled Make Visible that toggles the visibility of a p element based on the value of the visible variable. When the checkbox is checked, the p element will enter the DOM with a spinning animation that lasts for 8000 milliseconds and fade out smoothly when hidden.
<script>
let visible = false;
function typewriter(node, { speed = 1 }) {
const hasOneChild = node.childNodes.length === 1;
const isTextChild = node.childNodes[0].nodeType ===
Node.TEXT_NODE;
const isValid = hasOneChild && isTextChild;
if (!isValid) {
throw new Error(
'Transition can be applied only to elements' +
'with a single text node child'
);
}
const text = node.textContent;
const duration = text.length / (speed * 0.01);
return {
duration,
tick: t => {
const idx = Math.trunc(text.length * t);
node.textContent = text.slice(0, idx);
}
};
}
</script>
<label>
<input type='checkbox' bind:checked={visible} />
Make Visible
</label>
{#if visible}
<p transition:typewriter>
Hello, world!
</p>
{/if}
Javascript.svelte
In the code example above, a typewriter function accepts a node parameter representing the DOM element undergoing the transition. It also has an optional speed property. This function retrieves the text content of the node and calculates the duration of the transition based on the length of the text and the provided speed. It then returns an object containing a duration property and a tick function.
The duration property specifies the total duration of the transition, determining how long it takes for the typewriter effect to complete.
The tick function is called repeatedly throughout the transition and receives a progress value, denoted as t, ranging from 0 to 1. Within the tick function, the text content of the node is updated to display a portion of the original text based on the progress value t. This creates the typewriter effect, gradually revealing the text as if it is being typed.
By utilizing the typewriter function, you can apply the typewriter effect to the text content of a DOM element, with the option to customize the speed and duration.
In the markup, there is a checkbox input that is connected to the visible variable. When the checkbox is checked, the visible variable is set to true, which triggers the rendering of a p element. This element is associated with the typewriter transition function using the
transition:typewriter
directive. When the p element enters the DOM, the typewriter transition function is applied, resulting in the text appearing gradually as if it is being typed.
Key Blocks
Key blocks are essential for triggering transitions when the value of an expression changes. They facilitate dynamic updates to elements, ensuring that transitions occur when values change, rather than when elements are added or removed from the DOM.
<script>
import { onMount } from 'svelte';
let i = 0;
onMount(() => {
const interval = setInterval(() => i++, 5000);
return () => clearInterval(interval);
});
function typewriter(node, { speed = 1 }) {
const text = node.textContent;
const duration = text.length / (speed * 0.01);
return {
duration,
tick: t => {
const idx = Math.trunc(text.length * t);
node.textContent = text.slice(0, idx);
}
};
}
</script>
{#key i}
<p in:typewriter={{ speed: 0.5 }}>
Key block item {i}
</p>
{/key}
<p in:typewriter={{ speed: 0.5 }}>
Item without key block {i}
</p>
KeyBlocks.svelte
In this code example, the
{#key}
block is utilized to create a typewriter effect on the p element. By incorporating the{#key i}
block, Svelte recognizes that the element should be re-rendered whenever the value of i changes. This guarantees that the typewriter effect is applied to each new value of i, animating the text content accordingly.Without the
{#key}
block, Svelte would consider the content of the element to remain the same, disregarding changes in i, and thus preventing the desired typewriter animation from occurring.Both scenarios are presented for comparison.
Transition Modifiers
In Svelte 4 and beyond, transitions behave similarly to CSS, occurring when their direct containing block is added or removed. This differs from Svelte 3, where transitions were global by default, triggering when any block containing transitions was added or removed.
When applying transitions in Svelte 4+, there are 2 variations to consider, without any modifier and with the global modifier. The first variation aligns with the default behavior of transitions in Svelte 4+, while the global modifier emulates the global transition behavior seen in Svelte 3.
In Svelte 3, there were also 2 variations, without any modifier and with the local modifier. The first variation mirrored the default behavior of transitions in Svelte 3, while the local modifier imitated the global transition behavior found in Svelte 4+.
<script>
import { slide } from 'svelte/transition';
let items = ['Items 1', 'Items 2', 'Items 3'];
let visible1 = true;
let visible2 = true;
function addItem() {
items = [
...items,
`Item ${items.length + 1}`
];
}
</script>
<button on:click={addItem}>Add Item</button>
<div>
<div>
<label>
<input type='checkbox' bind:checked={visible1} />
Make Visible
</label>
{#if visible1}
{#each items as item}
<!--
In Svelte 4, it applies to a single p element.
In Svelte 3, it applies to each p element.
To achieve the Svelte 4 behavior in Svelte 3,
use "transition:slide|local".
-->
<p transition:slide>
{item}
</p>
{/each}
{/if}
</div>
<div>
<label>
<input type='checkbox' bind:checked={visible2} />
Make Visible
</label>
{#if visible2}
{#each items as item}
<!--
In Svelte 4, transition applies to each p element.
In Svelte 3, there is no global modifier.
-->
<p transition:slide|global>
{item}
</p>
{/each}
{/if}
</div>
</div>
<style>
div {
display: flex;
justify-content: center;
align-items: center;
gap: 1em;
}
div > div {
flex-direction: column;
padding: 1em;
border: 2px solid #000;
}
</style>
TransitionModifiers.svelte
In the provided code example, when the transition is applied globally, toggling the checkbox will make all items appear or disappear smoothly. The transition effect will be applied to the entire block containing the items, creating a cohesive animation for all items.
On the other hand, when the transition is applied locally, each item will have a smooth transition effect when added or removed. However, the block containing the items will not have a transition effect applied to it as a whole.
In both cases, adding a new item will trigger the transition for that item.
Deferred Transitions
Svelte's transition engine offers a useful feature known as deferred transitions, which enables coordination between multiple elements. For instance, there is a pair of todo lists where toggling a todo sends it to the opposite list. Instead of objects abruptly disappearing and reappearing, they smoothly move through a series of intermediate positions.
To achieve this effect, Svelte provides the crossfade function. This function generates 2 transitions called send and receive. When an element is "sent", it gracefully transforms to its counterpart's position and fades out. Conversely, when an element is "received", it smoothly moves to its new position and fades in.
import { writable } from 'svelte/store';
export function createTodoStore(initial) {
let uid = 1;
const todos = initial.map(({ done, description }) => {
return {
id: uid++,
done,
description
};
});
const { subscribe, update } = writable(todos);
return {
subscribe,
add: description => {
const todo = {
id: uid++,
done: false,
description
};
update($todos => [...$todos, todo])
},
remove: todo => {
update($todos => $todos.filter(t => t !== todo));
},
mark: (todo, done) => {
update($todos => [
...$todos.filter(t => t !== todo),
{ ...todo, done }
]);
}
};
}
todos.js
import { crossfade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
export const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
fallback(node, _params) {
const style = getComputedStyle(node);
const transform =
style.transform === 'none'
? ''
: style.transform;
return {
duration: 600,
easing: quintOut,
css: t => `
transform: ${transform} scale(${t});
opacity: ${t}
`
};
}
});
transition.js
<script>
import { send, receive } from './transition.js';
export let store, done;
</script>
<ul>
{#each $store.filter(todo => todo.done === done) as todo (todo.id)}
<li
class:done
in:receive={{ key: todo.id }}
out:send={{ key: todo.id }}
>
<label>
<input
type='checkbox'
checked={todo.done}
on:change={
e => store.mark(todo, e.currentTarget.checked)
}
/>
<span>{todo.description}</span>
<button on:click={() => store.remove(todo)}>
Remove
</button>
</label>
</li>
{/each}
</ul>
<style>
ul {
padding: 0;
}
li {
list-style: none;
display: flex;
align-items: center;
}
label {
width: 90%;
display: inline;
}
span {
flex: 1;
}
button {
margin-top: 0.5em;
cursor: pointer;
}
</style>
TodoList.svelte
<script>
import { createTodoStore } from './todos.js';
import TodoList from './TodoList.svelte';
const todos = createTodoStore([
{ done: false, description: 'Todo 1' },
{ done: false, description: 'Todo 2' },
{ done: true, description: 'Todo 3' },
{ done: false, description: 'Todo 4' },
{ done: false, description: 'Todo 5' },
{ done: false, description: 'Todo 6' }
]);
</script>
<div class='board'>
<input
placeholder='What needs to be done?'
on:keydown={(e) => {
if (e.key !== 'Enter') return;
todos.add(e.currentTarget.value);
e.currentTarget.value = '';
}}
/>
<div class='todo'>
<h2>Todo</h2>
<TodoList store={todos} done={false} />
</div>
<div class='done'>
<h2>Done</h2>
<TodoList store={todos} done={true} />
</div>
</div>
<style>
.board {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 1em;
max-width: 36em;
margin: auto;
}
.board > input {
font-size: 1em;
grid-column: 1/3;
padding: 0.5em;
margin: 0 0 1rem 0;
}
h2 {
font-size: 2em;
font-weight: 200;
}
</style>
DeferredList.svelte
In the example code above, the TodoList component represents a list of todos, with each todo item rendered as an li element. The component includes a send function responsible for handling the transition of a todo item from one list to another.
When invoked, the send function accepts 2 arguments, the todo object representing the specific item and the list representing the target list. Within the send function, a transition is triggered using the crossfade function. This crossfade function takes 3 arguments, the element to transition, the target position, and an optional configuration object. It returns a promise that resolves once the transition is complete.
In this case, the crossfade function smoothly transitions the todo item to its new position and fades it out when sent to the opposite list.
When a todo item is toggled, the send function is invoked.
This function examines the list argument to determine the target list and utilizes the crossfade function to transition the todo item accordingly, providing a visually appealing effect as items move between the Todo and Done.
Animate Directive
To ensure a seamless experience and enhance the overall illusion, it is crucial to incorporate motion into elements that are not currently undergoing transitions. This can be accomplished by using the animate directive. Svelte offers a convenient flip animation function, which can be utilized when you wish to add animations to a list of items that are being reordered. The animate: directive plays a key role in applying these animations to the elements.
import { writable } from 'svelte/store';
export function createTodoStore(initial) {
let uid = 1;
const todos = initial.map(({ done, description }) => {
return {
id: uid++,
done,
description
};
});
const { subscribe, update } = writable(todos);
return {
subscribe,
add: description => {
const todo = {
id: uid++,
done: false,
description
};
update($todos => [...$todos, todo])
},
remove: todo => {
update($todos => $todos.filter(t => t !== todo));
},
mark: (todo, done) => {
update($todos => [
...$todos.filter(t => t !== todo),
{ ...todo, done }
]);
}
};
}
todos.js
import { crossfade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
export const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
fallback(node, _params) {
const style = getComputedStyle(node);
const transform =
style.transform === 'none'
? ''
: style.transform;
return {
duration: 600,
easing: quintOut,
css: t => `
transform: ${transform} scale(${t});
opacity: ${t}
`
};
}
});
transition.js
<script>
import { flip } from 'svelte/animate';
import { send, receive } from './transition.js';
export let store, done;
</script>
<ul>
{#each $store.filter(todo => todo.done === done) as todo (todo.id)}
<li
class:done
in:receive={{ key: todo.id }}
out:send={{ key: todo.id }}
animate:flip={{ duration: 200 }}
>
<label>
<input
type='checkbox'
checked={todo.done}
on:change={
e => store.mark(todo, e.currentTarget.checked)
}
/>
<span>{todo.description}</span>
<button on:click={() => store.remove(todo)}>
Remove
</button>
</label>
</li>
{/each}
</ul>
<style>
ul {
padding: 0;
}
li {
list-style: none;
display: flex;
align-items: center;
}
label {
width: 90%;
display: inline;
}
span {
flex: 1;
}
button {
margin-top: 0.5em;
cursor: pointer;
}
</style>
TodoList.svelte
<script>
import { createTodoStore } from './todos.js';
import TodoList from './TodoList.svelte';
const todos = createTodoStore([
{ done: false, description: 'Todo 1' },
{ done: false, description: 'Todo 2' },
{ done: true, description: 'Todo 3' },
{ done: false, description: 'Todo 4' },
{ done: false, description: 'Todo 5' },
{ done: false, description: 'Todo 6' }
]);
</script>
<div class='board'>
<input
placeholder='What needs to be done?'
on:keydown={(e) => {
if (e.key !== 'Enter') return;
todos.add(e.currentTarget.value);
e.currentTarget.value = '';
}}
/>
<div class='todo'>
<h2>Todo</h2>
<TodoList store={todos} done={false} />
</div>
<div class='done'>
<h2>Done</h2>
<TodoList store={todos} done={true} />
</div>
</div>
<style>
.board {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 1em;
max-width: 36em;
margin: auto;
}
.board > input {
font-size: 1em;
grid-column: 1/3;
padding: 0.5em;
margin: 0 0 1rem 0;
}
h2 {
font-size: 2em;
font-weight: 200;
}
</style>
AnimationList.svelte
In this example, the TodoList component from the deferred transition example is changed to include the flip animation.
FLIP, which stands for First, Last, Invert, Play, is a technique used to create seamless animations. It involves defining the initial and final states of an element, determining the changes that occur between them, inverting those changes, and then playing the animation by transitioning from the inverted state to the final state. By applying transforms and opacity changes in reverse, the elements appear as if they are still in the initial position. This technique is especially beneficial when responding to user interactions and animating elements accordingly.
Actions
Actions act as functions that enable direct interaction with the DOM or facilitate side effects within components. They are commonly used for tasks such as manipulating elements, handling events, or integrating with external libraries. Although actions are not extensively employed, they provide a way to execute a function when an element is added to the DOM. In essence, actions can be seen as lifecycle hooks for individual DOM elements, like onMount or onDestroy, but tied specifically to the elements themselves rather than the entire components.
To define an action, you can specify the DOM element as a parameter, along with any optional parameters you may need.
The action function can then perform initialization logic on the element and optionally return an object that contains update and destroy handlers. These handlers allow you to manage any updates or clean-up operations related to the action.
To apply an action to a specific element, the use: directive is used.
export function resizeAction(node) {
// node has been mounted
function handleResize() {
const { width, height } = node.style;
if (!width || !height) {
node.textContent = 'Resize Me';
return;
}
node.textContent = `${width} x ${height}`;
}
const resizeObs = new ResizeObserver(handleResize);
resizeObs.observe(node);
node.textContent = 'Initial value';
return {
update: bgColor => {
/**
* Resizing will not trigger this method,
* only parameter changes.
*/
node.style.backgroundColor = bgColor;
console.log('Element parameter updated');
},
destroy: () => {
/**
* Node has been removed from the DOM,
* setting node.textContent will not have any effect.
*/
resizeObs.disconnect();
console.log('Element destroyed');
}
}
}
actions.js
<script>
// @ts-nocheck
import { resizeAction } from './actions';
let visible = true;
let bgColor;
</script>
<label>
<input type='checkbox' bind:checked={visible} />
Make Visible
</label>
<label>
Background Color:
<input type='color' bind:value={bgColor} />
</label>
{#if visible}
<div use:resizeAction={bgColor}></div>
{/if}
<style>
div {
border: 2px solid #000;
resize: both;
overflow: auto;
}
</style>
ResizableRectangle.svelte
In the code example above, the component controls the visibility and background color of a resizable div element. It achieves this by utilizing the resizeAction function from the
actions.js
file, which handles resizing events.The component includes a checkbox that allows to toggle the visibility of the div element, as well as a color input that enables the change of the div's background color. When the checkbox is checked, the div element is displayed and the resizeAction function is applied as a Svelte action.
This function leverages the ResizeObserver API to track resize events and update the div's text content with its current width and height. Additionally, the component provides an update function that allows users to modify the background color, and a destroy function that disconnects the ResizeObserver when the div is removed from the DOM.
CSS Preprocessor Integration
CSS preprocessors like SCSS/SASS, LESS, and Stylus offer developers powerful tools to enhance their CSS workflow.
SCSS/SASS provide features such as variables, nesting, mixins, and functions, making CSS code more maintainable. LESS, another popular preprocessor, offers similar capabilities to SCSS/SASS but with a slightly different syntax.
Stylus, known for its minimalistic approach, focuses on simplicity and flexibility, using indentation-based syntax for concise and expressive code.
To integrate any of these preprocessors, install the compiler and indicate the preprocessor language in the component style section using the lang
attribute, or import a preprocessor style file in the script section. This setup should seamlessly enable the preprocessor to function without the need for additional configuration steps.
Using a CSS preprocessor with Svelte can introduce additional complexity, potentially making your code harder to manage. It is important to consider whether the benefits of using a preprocessor outweigh the added complexity it may bring to your development process.
# SCSS/SASS
npm install -D sass
# LESS
npm install -D less
# Stylus:
npm install -D stylus
Install CSS preprocessor
<script>
// Import style file
import './styles.scss';
import './styles.sass';
import './styles.less';
import './styles.styl';
</script>
<!-- OR write component styles -->
<!-- Note, only one <style> tag per component is allowed -->
<style lang='scss'></style>
<style lang='sass'></style>
<style lang='less'></style>
<style lang='styl'></style>
<style lang='stylus'></style>
Component.svelte
Tailwind CSS Integration
Tailwind CSS is a utility-first CSS framework that sets itself apart from traditional frameworks by not offering pre-designed components. Instead, it provides a range of CSS utility classes that empower you to efficiently develop custom designs without being constrained by generic prebuilt components.
To integrate Tailwind CSS into a Svelte project, start by running npm install -D tailwindcss postcss autoprefixer
in the terminal.
PostCSS is a CSS transformation tool that operates with various plugins, with Autoprefixer being a plugin designed to automate the process of adding vendor prefixes to CSS code. These packages are typically required when integrating Tailwind into a project without utilizing the Tailwind CLI. Given that Tailwind CSS does not inherently support vendor prefixing, it is recommended to include Autoprefixer in the setup. Autoprefixer tracks caniuse.com for CSS properties that need prefixes to maintain consistent display across various browsers.
After installing the development dependencies, initialize Tailwind CSS by running npx tailwindcss init -p
in the terminal. This command initializes Tailwind CSS and generates a tailwind.config.js file along with a postcss.config.js, allowing you to customize Tailwind's and PostCSS's configuration settings.
In the tailwind.config.js file, add the purge
option to eliminate unused CSS by specifying the relevant files.
...
purge: ['./index.html', './src/**/*.{svelte,js,ts}'],
...
tailwind.config.js
In the postcss.config.js file, import tailwindcss, the tailwind.config.js file, and autoprefixer, and set up the plugins accordingly.
import tailwind from 'tailwindcss';
import tailwindConfig from './tailwind.config.js';
import autoprefixer from 'autoprefixer';
export default {
plugins: [tailwind(tailwindConfig), autoprefixer]
};
postcss.config.js
Once these steps have been finished, the setup should be considered successful. To make use of Tailwind CSS, you can incorporate the Tailwind CSS styles in Svelte components within the component's style section.
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>
Component.svelte
Alternatively, they can be included in a global CSS file, which can then be imported in main.js/main.ts.
@tailwind base;
@tailwind components;
@tailwind utilities;
app.css
import './app.css'
import App from './App.svelte'
const app = new App({
target: document.getElementById('app'),
})
export default app
main.js
Lazy Loading
Lazy loading is a technique that involves loading resources only when they are needed, rather than loading all resources at once when the page initially loads. This approach can help reduce the initial load time of a webpage by loading only the necessary resources first, with additional resources loading as the user interacts with the page. Lazy loading can be applied to components, images, and everything else that can be loaded on demand.
One practical application of lazy loading is utilizing the Intersection Observer API, which allows you to respond to the intersection between the browser's viewport and an HTML element. Viewport refers to the visible area of a web page to the user, without requiring scrolling or zooming. By detecting when a user is about to interact with a resource, you can effectively implement lazy loading.
<script>
import { onMount } from 'svelte'
let container = null;
let isIntersecting = false;
onMount(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
isIntersecting = entry.isIntersecting;
});
});
observer.observe(container);
});
</script>
<div bind:this={container}>
{#if isIntersecting}
<slot></slot>
{/if}
</div>
GenericLazyLoader.svelte
<script>
import { onMount } from 'svelte';
let message;
onMount(() => {
setTimeout(
() => message = 'Hello, world',
3000
);
});
</script>
<p>{message || 'Loading...'}</p>
Component.svelte
<script>
import Component from './Component.svelte';
import GenericLazyLoader from './GenericLazyLoader.svelte';
</script>
<!--
A substantial amount of text that
extends the page for scrolling.
-->
<GenericLazyLoader>
<Component />
</GenericLazyLoader>
<GenericLazyLoader>
<!-- svelte-ignore a11y-missing-attribute -->
<img src='https://picsum.photos/200' />
</GenericLazyLoader>
LazyLoading.svelte
In the code example above, 3 components are defined: GenericLazyLoader, Component, and LazyLoading, with LazyLoading serving as the entry-point.
The GenericLazyLoader component implements lazy loading using the Intersection Observer API to determine when its content should be displayed.
The Component component renders a delayed message within a p element after 3 seconds.
The LazyLoading component acts as the main component, importing the other 2 components and managing the lazy loading of both the Component and an image within GenericLazyLoader.
This setup ensures that components and images are loaded only when they are about to become visible in the viewport. The message in the Component will be displayed after a 3-second delay once it becomes visible on the page.
Consuming APIs
When it comes to consuming REST and GraphQL APIs, there is a variety of options available. For REST APIs, tools like the Fetch API or HTTP clients such as Axios are commonly used for handling HTTP requests. When working with GraphQL APIs, you can choose to utilize the Fetch API or the Apollo Client library.
Integrating API calls within the onMount lifecycle function enables data retrieval when the component is loaded. Another method involves using module context to access data that can be shared across all instances of the component. Employing {#await} blocks is valuable for effectively managing asynchronous operations. Triggering API requests through event handlers offers a user-centric approach to fetching data.
Storing API responses in a store not only ensures state persistence across component remounts but also helps maintain smooth data management processes.
Forms & Validation
In Svelte, you can easily handle form inputs, validation, and submission using reactive variables and event handlers. To implement form validation, you can track form field values with reactive variables and use conditional logic to check for errors based on validation rules. Error messages can be displayed based on validation results. Additionally, you can prevent the default form submission behavior and handle form submission asynchronously, validating inputs before submitting data. Svelte's reactivity makes it straightforward to update the UI based on user input and validation results, providing a seamless user experience.
import { writable } from 'svelte/store';
export const isLoggedIn = writable(false);
export const userDetails = writable({});
user.js
<script>
import { isLoggedIn, userDetails } from './user';
let values = {};
let errors = {};
async function register(data) {
// Send data to server
return new Promise(resolve => {
setTimeout(
() => {
isLoggedIn.set(true);
userDetails.set(data);
resolve();
},
1000
);
});
}
async function submitHandler() {
// Implement validation
// Use a validation library like Yup or Zod
const { email, password } = values;
let submit = true;
if (!email) {
errors.email = 'Email is required';
values.email = '';
submit = false;
}
if (email && !email.includes('@')) {
errors.email = 'Invalid email format';
values.email = '';
submit = false;
}
if (!password) {
errors.password = 'Password is required';
values.password = '';
submit = false;
}
if (password && password.length < 8) {
errors.password = 'Password must have ' +
'at least 8 characters';
values.password = '';
submit = false;
}
if (!submit) return;
errors = {};
await register(values);
}
</script>
<form on:submit|preventDefault={submitHandler}>
<h2>Register</h2>
<div>
<input
type='text'
name='email'
bind:value={values.email}
placeholder='Email'
/>
{#if errors.email}
<span>{errors.email}</span>
{/if}
</div>
<div>
<input
type='password'
name='password'
bind:value={values.password}
placeholder='Password'
/>
{#if errors.password}
<span>{errors.password}</span>
{/if}
</div>
<button type='submit'>Register</button>
</form>
<style>
form {
width: 50%;
border: 2px dashed #000;
display: flex;
flex-direction: column;
gap: 1em;
justify-content: center;
align-items: center;
}
input {
width: 20em;
display: block;
padding: 0.5em;
}
button {
margin: 1em;
}
span {
display: block;
color: red;
margin-top: 0.2em;
margin-left: 0.2em;
}
</style>
Register.svelte
<script>
import { isLoggedIn, userDetails } from './user';
function logout() {
$isLoggedIn = false;
$userDetails = {};
}
</script>
{#if $isLoggedIn}
<p>Email: {$userDetails?.email}</p>
<button on:click={logout}>Logout</button>
{/if}
Profile.svelte
<script>
import Profile from './Profile.svelte';
import Register from './Register.svelte';
import { isLoggedIn } from './user';
</script>
{#if $isLoggedIn}
<Profile />
{:else}
<Register />
{/if}
App.svelte
This code example showcases a simple user authentication system in a Svelte application.
In the App component, which is the main component, users are directed to either the Profile or Register component based on their login status.
The Register component manages a form, validating user inputs, and simulating the registration process.
The Profile component displays the user's email and provides a logout option.
The
user.js
file contains Svelte writable stores for tracking login status and storing user details.
Documenting
You can document your components by using specially formatted comments in editors that support the Svelte Language Server. Within the markup section of a component, the @component
tag inside an HTML comment can be utilized to describe the component. This documentation will be displayed when you hover over the component element. Markdown formatting can be used within the documentation comment to offer clear and structured information about the component, including usage examples and code blocks. Following this format enables you to efficiently document your Svelte components for improved comprehension and maintenance.
<script>
import User from "./User.svelte";
</script>
<!-- Hover over "User" -->
<User username='admin' />
HoverComment.svelte
Hovering over <User />
on VSCode
Debugging
Using console.log()
within the script section of a Svelte component is an effective way to log information to the console for debugging purposes. Additionally, by including the {@debug}
tag in the markup, you can pause execution and inspect specific values directly in the browser's developer tools. These methods are practical for debugging and enhancing your understanding of the data flow within components.
The following example code demonstrates
{@debug}
:
<script>
const user = {
firstName: 'John',
lastName: 'Doe',
location: {
country: 'France',
city: 'Paris'
}
};
</script>
<input bind:value={user.firstName} />
<input bind:value={user.lastName} />
<input bind:value={user.location.country} />
<input bind:value={user.location.city} />
{#if user}
{@const { firstName, lastName, location } = user}
{@const { country, city } = location}
<p>{firstName} {lastName} from {city}, {country}</p>
{/if}
{@debug user}
Debug.svelte
Testing
In a Svelte application, there are typically 3 main types of tests, unit tests, component tests, and end-to-end (e2e) tests.
Unit tests focus on validating the business logic in isolation, checking individual functions and edge cases. Component tests ensure that a component behaves as expected throughout its lifecycle. To test components, a tool providing a Document Object Model (DOM) is necessary. End-to-end tests are essential for testing the application as a whole, replicating user interactions in a production-like environment. These tests interact with a deployed version of the application to simulate user behavior.
For unit testing, you can utilize Vitest. When it comes to component testing, combining Vitest with svelte-testing-library is recommended. The svelte-testing-library offers utilities for testing Svelte components in a way that mirrors user interactions. For e2e testing, consider using Playwright or Cypress to simulate user interactions across your application.
For more detailed information on how to setup testing, check this post.
Routing
Routing refers to the process of determining how the application's UI should change in response to a change in the URL. It enables navigation between different pages or views without the need for a full page reload.
In Svelte, routing can be implemented using a routing library like svelte-routing or by manually managing the application state based on the URL. The svelte-routing library provides a declarative way to define routes and their corresponding components, making it easier to handle navigation within the application. To implement routing using svelte-routing, you would typically define routes using the Route component, specify the path and component to render for each route, and include a Router component to handle the routing logic. When a user navigates to a specific URL, the corresponding component defined in the route configuration will be rendered in the application.
For more information and examples about svelte-routing, visit this GitHub repository.
<script>
function navigate() {
window.location = '/#page';
}
</script>
<p>Home</p>
<button on:click={navigate}>Go to Page</button>
Home.svelte
<script>
function navigate() {
window.location = '/#';
}
</script>
<p>Page</p>
<button on:click={navigate}>Go to Home</button>
Page.svelte
<script>
function navigate() {
window.location = '/#';
}
</script>
<p>404 - Page Not Found</p>
<button on:click={navigate}>Go to Home</button>
404.svelte
<script>
import { onMount } from 'svelte';
import Error404 from './components/404.svelte';
import Home from './components/Home.svelte';
import Page from './components/Page.svelte';
const routes = {
'/': Home,
'page': Page,
'*': Error404
};
let currentRoute = '';
function handleRoute() {
const { hash } = window.location;
currentRoute = hash ? hash.slice(1) : '/';
}
onMount(() => {
handleRoute();
window.addEventListener('hashchange', handleRoute);
});
$: Component = routes[currentRoute] || routes['*'];
</script>
<svelte:component this={Component} />
Router.svelte
<script>
import Router from './Router.svelte';
</script>
<Router />
App.svelte
In the code example above, a simple SPA is implemented built using a custom Router component to navigate between components. The Router component defines the routing logic for the application. It sets up different routes mapping to specific components like Home, Page, and Error404. The current route is determined based on the hash part of the URL, and a listener is added to handle changes in the hash part.
The Home, Page, and 404 components contain simple components with some text and a button. When the button is clicked, it triggers a function that changes the hash part of the URL, simulating navigation within the SPA. If the route does not exist, the 404 component is rendered.
Overall, this example allows for basic client-side routing in a Svelte application.
Typescript Limitations
When using TypeScript in a Svelte application, there are certain limitations to be aware of TypeScript usage directly within the markup is restricted, meaning that type declarations and assertions are invalid within the markup section. Additionally, trying to incorporate TypeScript syntax directly into reactive declarations by adding type declarations will not work as expected. To address this, it is advised to define variables with types independently before incorporating them into reactive declarations for proper functionality.
Deploying
Deploying a Svelte application involves choosing a hosting provider that supports static sites or Node.js, setting up the hosting environment, and uploading the project. Well-known hosting providers such as Netlify, Vercel, Heroku, and AWS are favored options due to their tailored services for hosting Svelte applications effortlessly. These platforms offer efficient deployment procedures, scalability features, and additional tools to facilitate the seamless deployment and maintenance.
Runes
Svelte 5, currently in development and not yet available for production use, introduces runes as a set of primitives that help manage reactivity within components. Runes are symbol-like functions that provide instructions to the Svelte compiler, enabling the declaration of reactive state, derived state, and side effects.
For reactive state declarations, the $state
rune ensures deep reactivity for objects and arrays. There is also the $state.frozen
rune, designed for state that remains immutable, offering enhanced performance especially with large arrays or objects. Moreover, the $derived
rune serves for derived state, while the $effect
rune allows for executing side effects based on specific value changes.
The introduction of these features in Svelte 5 will provide developers with enhanced control over reactivity management within their components. For more detailed information and updates, visit this link.
Conclusion
In conclusion, this tutorial has provided a comprehensive overview of Svelte 4, covering a wide range of topics from project setup to advanced concepts like data binding, state management, and transitions.
While this tutorial does not include coverage of SvelteKit, the knowledge and skills acquired here serve as a valuable resource for developers looking to leverage the power and efficiency of Svelte in their projects.
As you begin your exploration of Svelte, may the insights shared in this tutorial help you in developing dynamic, responsive, and efficient web applications effortlessly.
Top comments (0)