As of November 16, 2024, this post provides accurate information about Svelte 4.
Table Of Contents
- Preface
- Introduction
- Use Cases
- Benefits & Drawbacks
- Project Setup
- Project Structure
- Component Structure
- State Management
- Props
- Data Binding
- DOM Binding
- DOM Element & Component Reference Binding
- Reactivity
- Reactivity Principles
- Templating
- Keyed {#each} Blocks In Templates
- 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 Techniques For Transitions
- Applying Transitions
- Transition Events
- Custom Transitions
- Key Blocks For Triggering Transitions
- Transition Modifiers
- Deferred Transitions
- Animate Directive
- Actions
- CSS Preprocessor Integration
- TailwindCSS Integration
- Shadcn Integration
- Flowbite Integration
- Icons
- Lazy Loading
- Consuming APIs
- Forms & Validation
- Routing
- Documenting
- Debugging
- Formatting & Linting
- Storybook
- Testing
- Accessibility
- Internationalization & Localization
- TypeScript Limitations
- Progressive Web App
- Deploying
- Imperative Component API
- Upgrading To Svelte 5
- 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. It is also advisable to have some background with frontend development frameworks for better comprehension of the given ideas.
Note that this tutorial does not include coverage of SvelteKit.
Introduction
Svelte is a JavaScript framework designed for building user interfaces. What sets Svelte apart from other frameworks is its method of code compilation; it compiles your code at build time rather than executing it in the browser. This results in highly optimized and efficient code, leading to faster load times and enhanced performance for applications.
During the development phase, developers write Svelte code using a component-based syntax and a reactive programming model. Svelte analyzes this code and compiles it into optimized JavaScript. When the application is ready for deployment, a build process is initiated, generating a bundle that can be served to the browser. This bundle typically includes the optimized JavaScript, CSS, and other necessary assets. Once the build is complete, the bundle is deployed to a web server. When users access the site, their browser downloads and executes the compiled JavaScript code.
Moreover, Svelte provides official support for TypeScript and includes built-in animation and transition features, eliminating the need for external libraries.
As you explore Svelte further, you might encounter SvelteKit, the official framework built on top of Svelte. While Svelte serves as a standalone framework for UI development, SvelteKit extends its functionality by providing additional features and tools to enhance the development experience.
Use Cases
Svelte is particularly effective for Single-Page Applications (SPAs), delivering smooth user experiences. Its reactive nature allows for interactive UI components that respond dynamically to user actions. Additionally, Svelte is well-suited for data visualization, rapid prototyping, and progressive web applications, thanks to its simplicity, quick setup, and small bundle size, which contribute to fast loading times and offline capabilities.
A Single-Page Application (SPA) is a type of application that loads a single HTML file to handle all server requests, dynamically updating the current page instead of loading entirely new pages from the server. This approach enables smoother interactions by loading only the essential content, minimizing load times and reducing the number of server requests.
Benefits & Drawbacks
Some key benefits of using Svelte include:
Component-based Architecture: Svelte promotes a component-based architecture, allowing you to create reusable, self-contained, and modular components. This approach not only helps keep your code organized but also makes collaboration among developers smoother. Additionally, by encouraging a clear separation of concerns, it ensures that your code remains maintainable and scalable, making it easier to adapt and grow as your project evolves.
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 your application's state reactively. With stores, you can define reactive data and subscribe to it, meaning that any changes made to that data will automatically update the user interface. This functionality simplifies the process of keeping your UI in sync with the underlying data, ensuring that it always reflects the latest state of the application.
Minimalism: Minimalism is a design philosophy focused on simplicity and clarity, and Svelte perfectly reflects this concept. Its syntax is designed to be concise and easy to understand, enabling you to achieve the same tasks with less code than many other frameworks. This approach not only makes your code clearer and more user-friendly but also helps new developers get up to speed more quickly, making the learning experience less intimidating.
Framework Size: Svelte is designed to be lightweight, meaning it does not include many extra dependencies. This can simplify your development process by reducing project complexity. With a core framework that remains lightweight, you gain more control and can make thoughtful decisions about which additional tools or libraries to incorporate. This flexibility allows you to select only the specific features you require, avoiding the challenges of a potentially bloated ecosystem.
Bundle Size: Svelte compiles your code at build time, producing highly optimized JavaScript and smaller bundle sizes compared to other frameworks. These smaller bundle sizes contribute to faster load times, as there is less data to transfer over the network, and enhance performance, allowing the browser to parse and execute the code more efficiently.
Compatibility: Svelte works well with a variety of JavaScript and TypeScript libraries and tools, aligning with the established standards of the JavaScript and TypeScript ecosystem.
Accessibility Checks: While Svelte doesn't have built-in accessibility checks, it provides warnings at compile time if you create markup that may be inaccessible.
While Svelte has many benefits, like any framework, it also has some potential drawbacks:
Small(er) Community & Ecosystem: The Svelte ecosystem may not be as extensive as those of more established frameworks, but it has been steadily gaining popularity. There are several third-party libraries and tools designed specifically for Svelte, including Svelte Material UI, Svelte Charts, and Svelte Testing Library, among others. These libraries enhance functionality and provide convenience for developing Svelte applications.
Developer Experience (DX): Svelte presents unique concepts and syntax that may feel different from those of other frameworks. While it uses familiar HTML, CSS, and JavaScript/TypeScript, Svelte has its own method for constructing UIs. Instead of depending on a virtual DOM or manual state management, it introduces reactivity at the language level, which can be a significant shift in thinking for developers. Component composition is handled through slots and props, rather than a strict parent-child relationship. Additionally, Svelte takes a compile-time approach, where components are transformed into efficient JavaScript code during the build process, contrasting with frameworks like React that perform updates at runtime.
No Older Browser Support: Svelte leverages modern features like reactive updates and JavaScript modules, which provide efficient and dynamic updates to the UI. However, it is important to recognize that not all browsers fully support these modern features, older browsers may have limited or no support. While Svelte does not natively support older browsers, you can use Babel in the build process to ensure broader compatibility. Babel is a JavaScript compiler that can transpile modern code into a format that older browsers can understand. By integrating Babel with Svelte, you can adapt your code to be compatible while still taking advantage of Svelte's modern features. Furthermore, if you choose to use Vite for building your Svelte applications, you will not need to worry about additional Babel configuration, as Vite automatically handles the transpilation process using Babel under the hood.
Tooling: Svelte does not have an official CLI tool for project scaffolding, unlike some other frameworks. However, community-driven solutions like degit and create-vite allow for the quick setup of a new Svelte project with a basic folder structure. In terms of debugging and testing, Svelte offers a development mode that provides helpful error messages and warnings during the development process. Additionally, there are specialized testing frameworks, such as Svelte Testing Library, designed specifically for unit testing Svelte components. Furthermore, svelte-add is a tool that enables the integration of features and functionalities into Svelte applications.
Project Setup
The modern method for creating a Svelte project 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 initiate a new project with Vite, which offers a simple setup process that includes built-in support for Svelte, as well as features such as a fast development server and optimized production builds. This command fetches the latest version of Vite from the npm registry and guides you through the configuration options. During project creation, Vite allows you to choose between JavaScript and TypeScript. It is important to note that this command creates a basic Svelte project without SvelteKit. Once your project is set up, you can run npm run dev
to launch the development server.
foo@bar:~$ npm create vite@latest
foo@bar:~$ cd my-app
foo@bar:~$ npm install
foo@bar:~$ npm run dev
Create a JavaScript/TypeScript project with Vite
Project Structure
In a Svelte project, the src folder typically includes two key files, main.js
, or main.ts
for TypeScript, and App.svelte
.
The main.js
or main.ts
file is 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 serves as the main component that establishes the foundation for the application's user interface. It defines the overall layout and structure, importing and rendering other components as needed. Each Svelte component contains its own scoped CSS styles defined within the component file. Furthermore, Svelte projects frequently include a file for application-wide styles.
For projects created with degit, this file can typically be found at public/global.css
, while for Vite projects, it is typically located at src/app.css
.
Svelte provides flexibility in naming conventions and file structure, enabling developers to organize their code according to the specific needs of their project.
Mounting a component refers to the process of attaching it to a specific element within the DOM. This step makes the component visible and interactive on the web page. When the App component is mounted, it serves as the entry point for rendering the entire application, allowing it to manage the display and functionality of child components, as well as the overall user interface.
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 or 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(ComponentName) 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 or TypeScript in one file, Svelte simplifies development and encourages component reusability.
<script>
let name = "World";
</script>
<p>Hello, {name}</p>
<style>
p {
font-weight: 700;
}
</style>
JavaScript component featuring all sections
<script lang="ts">
let name: string = "World";
</script>
<p>Hello, {name}</p>
<style>
p {
font-weight: 700;
}
</style>
TypeScript component featuring all sections
State Management
State refers to the data that captures the current condition or snapshot of an application at any given moment. It includes information such as user inputs, retrieved data, selected options, and any other pertinent data that affects the application's behavior and appearance.
State management involves the techniques, patterns, and tools that enable developers to manage and update this state in a consistent and efficient way. Svelte employs stores as reactive data containers that support state sharing and observation among components. By defining reactive variables and storing values in these stores, automatic state updates occur when changes are made. This centralized approach simplifies state management and improves code organization.
Additional 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 contains these children 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 only impact the local copy and do not change the original values passed by the parent.
There are two special objects for accessing component props, $$props
and $$restProps
. $$props is an object that contains all the props passed to a component as key-value pairs. $$restProps is used to access any additional props that are passed but not explicitly defined in the component for its child. $$props contains $$restProps, but not vice versa.
<script lang="ts">
export let username: string, email: string;
export let group = "Users";
console.log(username === $$props.username);
console.log(email === $$props.email);
console.log(group !== $$props.group);
</script>
<p>Username: {username}</p>
<p>Email: {email}</p>
<p>Group: {group}</p>
<p>Status: {$$restProps.status ?? "Offline"}</p>
User.svelte
<script lang="ts">
import User from "./User.svelte";
const email = "user@mail.com";
const group = "Super Users";
</script>
<!-- Passing all props -->
<User username="admin" email="admin@mail.com" group="Admins" />
<!-- Passing all props except for the "group" prop, which has a default value -->
<User username="user" email="user@mail.com" />
<!--
The "email" prop is assigned the value of variable email.
The "group" prop is assigned the value of the variable with the same name, group.
"{group}" is shorthand for "group={group}".
-->
<User username="user" email={email} {group} />
<!-- User with an extra "status" prop -->
<User username="user" {email} status="Online" />
<!-- Spread props -->
<User {...user} />
<!--
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}
/>
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, and group 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.
Data Binding
Parent components act as higher-level components that manage the state and behavior of one or more child components. They pass data to child components through props, which are variables or values that the child components can access and use. In contrast, child components serve as lower-level components that receive data from their parent components via props to display content or perform specific tasks. Child components can trigger events or invoke functions in their parent components to communicate changes and update shared data.
There are two methods for updating data between components, uni-directional binding and bi-directional binding. Uni-directional binding, or one-way binding, allows data to flow exclusively from a parent component to a child component. Changes made in the parent component are reflected in the child component, ensuring a structured data flow. On the other hand, bi-directional binding, or two-way binding, enables data exchange between a parent and child component in both directions. Modifications in either component prompt updates in both, ensuring synchronized data. This approach is beneficial in situations where immediate updates in one component must be reflected in the other.
When a parent component passes a value through props to a child component, and the child component calls a function in the parent component with that value, it creates a scenario where both values reference the same data. This situation can resemble two-way binding, as changes in one component can affect the other. Conversely, if the data passed between components is different or unrelated, it establishes two distinct one-way bindings, one from the parent component to the child component and another from the child component to the parent component. In this case, each component operates independently, with changes in one component not directly impacting the other.
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 follows the principles of uni-directional data flow.
Svelte promotes uni-directional binding, which enhances clarity and simplifies application debugging. However, it also provides features such as event handling and the bind:
directive to enable bi-directional binding when necessary.
<script lang="ts">
export let count: number;
export let updateValue: (count: number) => void = () => {};
</script>
<div>
<p>Child Counter: {count}</p>
<button on:click={() => updateValue(++count)}>Increment</button>
</div>
Child.svelte
<script lang="ts">
import Child from "./Child.svelte";
let count = 0;
const updateFn = (cnt: number) => (count = cnt);
</script>
<div class="parent">
<p>Parent Counter: {count}</p>
<button on:click={() => count++}>Increment</button>
</div>
<!-- One-way binding. "{count}" is shorthand for count="{count}". -->
<Child {count} />
<!-- Two-way binding. "bind:count" is shorthand for "bind:count={count}". -->
<Child bind:count />
<!-- Two one-way bindings resulting in a two-way binding -->
<Child {count} updateValue={updateFn} />
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, demonstrating 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 callback 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 two 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.
An important aspect to keep in mind is that updating the
count
variable in the Parent component will trigger a re-rendering of all Child components that receive thecount
prop. As a result, 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 lang="ts">
let value = "Default value";
let numValue: number, rangeValue: number;
let checked = true;
let selected: string, selectedRadio: string;
let selectedOptions: string[] = [];
let checkedOptions: string[] = [];
</script>
<!-- Binding between the "value" attributes and component's "value" property -->
<p>{value}</p>
<input bind:value />
<textarea bind:value></textarea>
<!-- Svelte automatically handles the conversion of data types -->
<p>{numValue ?? "-"} + {rangeValue ?? "-"} = {numValue + rangeValue || "--"}</p>
<input type="number" bind:value={numValue} />
<input type="range" bind:value={rangeValue} />
<label>
<!-- Bind "checked" attribute to toggle button's "disabled" attribute -->
<input type="checkbox" bind:checked />
{!checked ? "Enable" : "Disable"} Button
</label>
<button disabled={!checked}>Button</button>
<!-- Bind to display the selected choice -->
<p>Selected: {selected}</p>
<select bind:value={selected}>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
<!-- Bind to an array to display all selected choices -->
<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>
<!-- Bind a group of checkbox inputs, allowing multiple selections -->
<p>Checked: {checkedOptions.join(", ")}</p>
<input type="checkbox" value="A" bind:group={checkedOptions} />
<input type="checkbox" value="B" bind:group={checkedOptions} />
<input type="checkbox" value="C" bind:group={checkedOptions} />
<!-- Bind a group of radio buttons, allowing only one selection at a time -->
<p>Selected: {selectedRadio || "None"}</p>
<input type="radio" value="A" bind:group={selectedRadio} />
<input type="radio" value="B" bind:group={selectedRadio} />
<input type="radio" value="C" bind:group={selectedRadio} />
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. The content of thevalue
variable is dynamically displayed in real-time, showcasing the 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 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
selectedRadio
, allowing only one selection at a time. This enforces exclusivity among radio options.
DOM Element & Component Reference Binding
Reference binding, enabled by bind:this
, allows you to link DOM elements and component references to variables, giving you direct access to their properties and methods. This feature simplifies the interaction and manipulation of Svelte components and DOM elements.
<script lang="ts">
export let content = "";
export function clear() {
content = "";
}
</script>
<p>{content}</p>
Paragraph.svelte
<script lang="ts">
import Paragraph from "./Paragraph.svelte";
let content: string;
let p: Paragraph;
let div: HTMLDivElement;
</script>
<!-- Bind to component reference -->
<Paragraph bind:content bind:this={p} />
<textarea bind:value={content}></textarea>
<button on:click={p.clear}>Clear Paragraph</button>
<!-- Bind to DOM element -->
<div bind:this={div}>Content</div>
<button on:click={() => (div.style.background = "red")}>Change Color</button>
App.svelte
The code example above demonstrates two distinct uses of the
this
binding. Initially, it binds a component reference by associating the Paragraph component instance with the variablep
. This connection allows 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 used to connect the div DOM element to the variablediv
. This association allows for direct manipulation of the DOM element through the div variable. As a result, clicking the button changes the background color of the div to red.
Reactivity
Reactivity is a fundamental aspect of Svelte, allowing the DOM to reflect changes in the application's state in real-time. This feature ensures that the user interface stays updated with any data modifications resulting from user interactions or other events. Svelte's reactivity system operates by monitoring the dependencies between variables and the UI elements that depend on them. When a variable is modified, Svelte selectively updates only the necessary parts of the DOM to display the new value, eliminating the need to re-render the entire component. This focused updating method minimizes unnecessary computations, enhances performance, and contributes to a smooth user experience. Svelte utilizes several techniques to achieve reactivity, including reactive assignments, reactive statements, and reactive declarations.
Reactive assignments automatically trigger updates in the DOM whenever a variable in the markup is reassigned. This means that any change to the variable will result in the corresponding UI elements being refreshed to reflect the new value, ensuring that the user interface remains in sync with the application's state.
On the other hand, reactive declarations require a variable or expression to be prefixed with $:
to create a subscription to that variable. This subscription ensures that any changes to the variable will trigger a re-render of the component.
It is important to differentiate between reactive assignments, which manage variable reassignment directly in the markup, and reactive declarations, which are variables that update in response to changes in the corresponding right-hand side variable. This differentiation helps clarify how each mechanism contributes to the reactivity of the application.
Reactive statements, marked by $:
preceding a code block, subscribe to a variable within that block, causing the code to execute whenever the variable changes.
Although reactive declarations and reactive statements are sometimes used interchangeably, it is important to recognize their distinct functions. Reactive declarations are used to create variables, while reactive statements are designed to execute code blocks in response to changes in variables.
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 lang="ts">
let message = "Initial value";
let counter = 0;
// Reactive declaration
$: doubled = 2 * counter;
// 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: {doubled}</p>
<!-- Reactive assignment. "counter++" translates to "counter = counter + 1" under the hood. -->
<button on:click={() => counter++}>Increment</button>
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;
sets up a reactive declaration, ensuring that whenever thecounter
variable changes,doubled
will update to twice the value ofcounter
.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 component 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 0 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 Principles
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-rendering.
Additionally, if a function parameter shares the same name as a top-level variable, the parameter may shadow that variable, leading to unexpected behavior. To maintain proper reactivity, it is recommended to use distinct names for variables and parameters.
<script lang="ts">
let stack = [0, 1, 2];
let n = 2;
</script>
<p>n: {n}</p>
<p>Stack top: {stack.at(-1)}</p>
<button on:click={() => stack.push(++n)}>Non-working</button>
<button on:click={() => (stack = [...stack, ++n])}>Working</button>
Triggering reactivity on arrays
In the code above, the
stack
variable is initialized as an array[0, 1, 2]
, andn
is set to2
. The current value of n, representing the last value that has been added to the stack, is displayed, along with the top element of the stack. Two buttons, Non-working and Working are present.The Non-working button pushes the incremented value of
n
into thestack
array but fails to update the displayed stack top. In contrast, the Working button correctly updates the UI by adding the incrementedn
value to a new array created using the spread operator.The problem with the Non-working button arises from directly mutating the original stack array with the push method, which does not trigger Svelte's reactivity system to update the UI. In contrast, the Working button creates a new array by spreading the elements of the original stack and appending the incremented n value. This approach properly updates the stack array, triggering Svelte's reactivity based on assignment.
<script lang="ts">
const user = {
location: {
country: "France",
},
};
let nonWorkingLocation = user.location;
let workingLocation = user.location;
let value: string;
function nonWorkingSet(country: string) {
user.location.country = country;
}
function workingSet(country: string) {
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={() => nonWorkingSet(value)}>Non-working</button>
<button on:click={() => workingSet(value)}>Working</button>
Triggering reactivity on objects
In the code above, the
user
object contains a nestedlocation
object with acountry
property set toFrance
. Two variables,nonWorkingLocation
andworkingLocation
, are initialized to reference theuser.location
object. Avalue
variable is declared to store the input value.There are two functions, nonWorkingSet, which updates the
user.location.country
directly with the input country, and *workingSet, 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 and Working, trigger the respective functions to update the country value in theuser.location
object. The difference between the Non-working and Working functions lies in how they handle updating the workingLocation variable.
The Non-working function directly modifies the user.location.country
, while the Working 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 lang="ts">
let stack = [0, 1, 2];
let n = 2;
let topVariable = stack.at(-1);
$: topReactiveDeclaration = stack.at(-1);
function topFunction() {
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): {topReactiveDeclaration}</p>
<p>Stack top (Function): {topFunction()}</p>
<button on:click={() => (stack = [...stack, ++n])}>Push</button>
Triggering reactivity on functions
In this code example, there is an array named
stack
initialized as[0, 1, 2]
, and the variablen
is set to2
. The current value ofn
indicates the last value that has 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
topReactiveDeclaration
, which tracks the value of the top element of the stack with the expression$: topReactDecl = stack.at(-1)
.Additionally, a function named
topFunction
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 for topVariable` and topFunction.Reactivity is not triggered for topVariable
and **topFunction** because they are not reactive in nature. The topVariable variable simply references the top element of the stack using
stack.at(-1)` when it is declared. It does not have any mechanism to automatically update its value when the stack changes. The topFunction function returns the value of the top element of the stack when called. Similar to topVariable, it does not have built-in reactivity to update its value when the stack changes.On the other hand, topReactiveDeclaration 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, topReactiveDeclaration 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 lang="ts">
type User = { username: string; status: "Online" | "Offline" };
function nonWorkingLogout(user: User) {
user = { ...user, status: "Offline" };
}
function workingLogout(loggedUser: User) {
user = { ...loggedUser, status: "Offline" };
}
let user: User = {
username: "user",
status: "Online",
};
</script>
<p>{user.username} is {user.status}</p>
<button on:click={() => nonWorkingLogout(user)}>Non-working</button>
<button on:click={() => workingLogout(user)}>Working</button>
Shadowing reactivity
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 outer user 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 outer user 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 button is clicked, the status of the user object does not change to Offline as intended due to variable shadowing. On the contrary, clicking the Working button successfully updates the status of the user object to Offline because the outer user variable is modified within the function.
Templating
Svelte's templating system offers a clear and user-friendly way to create reactive web applications. In the component markup, the {variable}
syntax allows for variable interpolation, which enables the rendering of dynamic content that updates reactively. The {@html variable}
tag enables the inclusion of raw, unsanitized HTML content, but it is important to exercise caution to avoid potential XSS vulnerabilities.
Svelte supports several features for managing templates, including conditional rendering, looping, and promise handling. Conditional rendering can be implemented using the {#if}
block statement to display content based on specific conditions, with options for {:else if}
and {:else}
to handle multiple scenarios. The {#each}
block statement is used for iteration, allowing you to render markup for arrays or iterable objects. For promise handling, the {#await}
block statement effectively manages promises, providing separate sections for pending, resolved, and rejected states.
The {@const expression}
tag allows you to define local constants within {#if}
, {:else if}
, {:else}
, {#each}
, {:then}
, and {:catch}
blocks, which improves readability and efficiency by reducing redundant calculations.
Svelte also features the class:
directive for dynamically including CSS classes, enabling style adjustments based on specific conditions. The style:
directive allows the transfer of CSS variables to CSS properties, making it easier to apply dynamic styling to DOM elements. Additionally, Svelte promotes reusability and consistency through component styles, passing CSS variables to child components to ensure a unified design experience across the application.
It is worth noting that CSS is scoped within each component to prevent unintended style conflicts. When it is necessary to style elements outside a component's scope, the :global()
directive can be employed to apply CSS rules throughout the application. However, it is recommended to use this feature moderately to maintain encapsulation and minimize CSS leakage, which helps avoid style conflicts between components.
<script lang="ts">
const item = "<strong>Hello, world</strong>";
</script>
<p>Render as text: {item}</p>
<p>Render as HTML: {@html item}</p>
Variable interpolation
<script lang="ts">
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}
{#if count > 100}
<p>More than 100</p>
{/if}
Conditional rendering
<script lang="ts">
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}
Iteration over an array to render each element
<script lang="ts">
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}
Using object destruction and local constants in the markup
<script lang="ts">
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>Waiting to resolve...</p>
{:then data}
<p>{data}</p>
{/await}
<!-- Handle resolved state only -->
{#await promiseThatResolves then data}
<p>{data}</p>
{/await}
<!-- Handle pending, resolved, and rejected states -->
{#await promiseThatRejects}
<p>Waiting to resolve...</p>
{:then data}
Rendering based on promise status
The example bellow illustrates the
class:
directive,style:
directive, and component styles:
<script lang="ts">
export let label: string;
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: var(--color-true);
}
.state-false {
background: var(--color-false);
}
.toggled {
background: brown;
}
</style>
ToggleButtons.svelte
<script lang="ts">
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>
App.svelte
Here is an example of CSS leakage using the
:global()
directive:
<p>Child Component</p>
Child.svelte
<script lang="ts">
import Child from './Child.svelte';
</script>
<p>Parent Component</p>
<Child />
Parent.svelte
<script lang="ts">
import Parent from "./Parent.svelte";
</script>
<div>
<Parent />
</div>
<style>
/**
* Limit :global() rules as much as possible with selectors and combinators.
* Apply to p elements that follow another p element and have a div parent.
*/
div > :global(p + p) {
background-color: red;
color: blue;
}
</style>
App.svelte
The
div > :global(p + p)
rule targets the Child component.
Keyed {#each} Blocks In Templates
In an {#each}
block, the key attribute uniquely identifies each item in the array during rendering. This allows Svelte to efficiently update and re-render the list when changes occur. While the array index can sometimes be used as a key, it does not always ensure uniqueness, particularly with dynamic array modifications. It is advisable to link the key attribute directly to the object by referencing a property that is guaranteed to be unique, ensuring that each item has a distinct identifier.
By providing a key, Svelte can map each rendered item to a specific identifier. When the array is modified, Svelte uses these keys to efficiently determine which blocks should be removed from the DOM. If a key is absent in the updated array, the corresponding item will be removed from the DOM. On the other hand, if the key remains the same while the properties are updated, Svelte will adjust the properties of the existing item rather than recreating it. To prevent unintended behavior during array manipulations, it is advisable to utilize keyed {#each}
blocks.
<script lang="ts">
// Updated when a prop changes in parent component
export let username: string, status: string;
// Set during component creation and not updated afterwards
let message = `${username} is ${status}`;
</script>
<!-- Note that <li>{username} is {status}</li> will work correctly -->
<li>{message}</li>
User.svelte
<script lang="ts">
import User from "./User.svelte";
let users = [
{ id: 1, username: "user1", status: "Inactive" },
{ id: 2, username: "user2", status: "Active" },
{ id: 3, username: "user3", status: "Inactive" },
{ id: 4, username: "user4", status: "Active" },
{ id: 5, username: "user5", status: "Inactive" },
];
function deleteInactive() {
users = users.filter((user) => user.status === "Active");
}
</script>
<p>Keyed:</p>
<ul>
<!-- user.id serves as the key, specified as (user.id) -->
{#each users as user (user.id)}
<User username={user.username} status={user.status} />
{/each}
</ul>
<p>Non-keyed:</p>
<ul>
{#each users as user}
<User username={user.username} status={user.status} />
{/each}
</ul>
<button on:click={deleteInactive}>Delete</button>
App.svelte
The code example above demonstrates the use of keyed
{#each}
blocks. It includes a User component with three variables,username
andstatus
, which update with changes in the parent component, andmessage
, which is set during component creation and remains constant.A list item displays the formatted message variable. The App component utilizes an array of users to generate
User
components.The users are shown in two sections, one using the keyed approach and the other using the non-keyed approach. There is a Delete button that removes the users in the array with an
"Inactive"
status, triggering a UI re-render. In this case, User components are not destroyed and recreated. Instead, they are updated. The message variable will not be reassigned during this process.When the array is updated, the keyed approach effectively displays the User components. However, in the non-keyed approach, the old value for message appears in each User as items shift within the array. This problem occurs because Svelte cannot detect the dependency between the username, status, and message variables, resulting in a mix-up in the non-keyed approach.
The final result for the keyed approach is
user2 is Active - user4 is Active
, while for the non-keyed approach, it displaysuser1 is Active - user2 is Inactive
. This inconsistency occurs because the length of the array has changed, but Svelte cannot identify which specific objects were modified. In the keyed approach, Svelte effectively tracks the components using their unique keys, ensuring the correct display of updated values. However, the non-keyed approach does not have this tracking capability, leading to the display of outdated or incorrect information.
Events
Events are actions that occur within a web application, including user interactions like clicks, mouse movements, and keyboard inputs. Svelte uses events to create interactive user interfaces by executing specific code in response to these actions. Additionally, developers can trigger events programmatically, allowing them to simulate user interactions or implement custom event handling by creating and dispatching events on components and DOM elements.
In Svelte, events are handled using the on:
directive, which enables the framework to listen for both standard DOM events and custom events on elements. These events act as communication channels between different elements and components, with custom events being utilized for passing data between child and parent components. When using the on:event-name
directive, a function can be assigned as the value, which will be called after the event is dispatched, receiving an event object that contains details about the event.
The most common events to handle include: click, dblclick, mouseup, mousedown, mouseover, mousemove, mouseout, keyup, keydown, keypress, select, change, submit, reset, focus, focusin, focusout, blur, touchstart, touchend, touchmove, touchenter, touchleave, pointerup, pointerdown, pointermove, pointercancel, pointerover, pointerout, pointerenter, pointerleave, drag, dragstart, dragenter, dragleave, dragover, dragend, drop, resize, scroll
<button on:click={() => console.log("Clicked")}>Click</button>
<div
on:mouseenter={() => console.log("Hovered")}
on:mouseleave={() => console.log("Unhovered")}
>
Hover Over Here
</div>
DOM event handling
DOM Event Modifiers
Modifiers in DOM event handlers provide greater control and customization, enabling more precise event handling. These modifiers offer additional capabilities for managing event behavior effectively.
For example, the preventDefault modifier allows you to invoke e.preventDefault()
before executing the handler, which prevents the default behavior of the event. The stopPropagation modifier stops the event from moving to the next element by calling e.stopPropagation()
. The passive modifier improves scrolling performance for touch and wheel events, while 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 executed once. The self modifier ensures that the handler is only activated if e.target matches the element itself. Lastly, the trusted modifier activates the handler only if e.isTrusted is true, indicating that the event was triggered by a user action rather than through code. Chaining these modifiers enables a more sophisticated approach to event handling, providing enhanced precision and control.
The capture phase and bubbling phase are two stages of event propagation in the DOM during event handling. In the capture phase, an event starts at the top of the DOM tree and travels down to the target element that triggered the event, allowing parent elements to intercept the event before it reaches the target.
On the other hand, in the bubbling phase, the event moves back up from the target element to the root of the DOM tree, enabling parent elements to respond to events triggered by their child elements. This two-phase system allows for more flexible and interactive web applications by giving developers control over how and when events are handled.
For example, imagine a nested HTML structure that includes an outer div, an inner div, and a button positioned within the inner div. When the button is clicked, the event first travels through the capture phase, starting from the top of the DOM tree. If an event listener is set on either the outer or inner div with the capture flag enabled, it intercepts the event as it moves toward the button. Upon reaching the button, it invokes its own event listener if one has been defined. Following that, during the bubbling phase, the event travels back up through the DOM, allowing the outer or inner div without the capture flag enabled to respond to the event.
<button on:click|once|capture={() => console.log("Hello, world")}>
Click
</button>
DOM event modifier chaining
Component Dispatched Events
Components can communicate with one another by dispatching custom events, which promotes a decoupled approach for flexible interaction. In Svelte, you can use the createEventDispatcher function to dispatch an event. This function returns a dispatch function that allows you to send custom events along with optional data payloads.
On the receiving end, you can use the on:event-name
directive to listen for and handle these custom events. This allows you to respond to events and access any data passed with them using e.detail
. By utilizing custom events, Svelte components can effectively coordinate their actions.
Unlike DOM events, event bubbling, where events propagate from child components to parent components, does not automatically occur with component events. If you want to listen to an event from a deeply nested component, intermediate components must forward the event to the parent component. This process, known as event forwarding, enables event handling at different levels within the component hierarchy. It is important to note that event forwarding also applies to DOM events.
The example bellow showcases component event dispatching, event forwarding, and event handling:
<script lang="ts">
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 lang="ts">
import Inner from "./Inner.svelte";
</script>
<!-- Forward component dispatched event instead of handling it -->
<Inner on:data />
Outer.svelte
<script lang="ts">
import Inner from "./Inner.svelte";
import Outer from "./Outer.svelte";
</script>
<Inner on:data={(e) => console.log("Inner:", e.detail.message)} />
<Outer on:data={(e) => console.log("Outer:", e.detail.message)} />
App.svelte
The example bellow 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 lang="ts">
import ForwardButton from "./ForwardButton.svelte";
import HandleButton from "./HandleButton.svelte";
const inputHandler = (e: Event) =>
console.log((e.target as HTMLInputElement).value);
</script>
<!-- Handling DOM event -->
<input on:input={inputHandler} />
<!-- 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")} />
App.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 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 before onMount, the beforeUpdate runs, while after onMount, the afterUpdate runs.
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 lang="ts">
import { onMount, onDestroy } from "svelte";
export let name;
onMount(() => {
console.log("Item created");
return () => console.log("Item destroyed #2");
});
onDestroy(() => console.log("Item destroyed #1"));
</script>
<li>{name}</li>
Item.svelte
<script lang="ts">
import { onMount, beforeUpdate, afterUpdate } from "svelte";
import Item from "./Item.svelte";
let show = true;
let items = [];
// Simulate an API call
function fetchData(): Promise<{ data: string[] }> {
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 lang="ts">
import { tick } from "svelte";
let count = 0;
let p: HTMLParagraphElement;
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>
In the code example above, there is a
<p>
element bound to the variable p, a variable count initialized to 0, and an Increment button that, when clicked, triggers the increment function.Within the increment 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
In Svelte, stores are objects created to hold particular values and include a subscribe method that enables components to receive updates whenever there are 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.
Stores are an excellent method for maintaining state in a Single Page Application (SPA). If you want to maintain the state between page reloads, you can use browserStorage or sessionStorage.
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: (num: number) => void) {
// 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.ts
<script lang="ts">
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: number;
let doubledCountValue: number;
let randomValue: number;
// 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</button>
<button on:click={() => count.set(0)}>Reset</button>
App.svelte
In the code above, the stores.ts 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
, created 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 manages the continuous generation of random numbers and clears the interval upon component destruction.In the context of a Svelte readable store, the start function is invoked when a component subscribes to the store. It takes a set parameter, which is a function used to update the store's value. On the other hand, the stop function is called when there are no longer any subscribers or when the component is destroyed, serving to clean up resources. Together, these functions manage the lifecycle of the store, ensuring that it updates correctly while also preventing memory leaks when it is no longer in use.
The App 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 used 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 to update the writable storecount
by incrementing its value or resetting it to 0, triggering updates in all related stores.
Custom Stores
By implementing the subscribe method, developers can effectively create custom stores that include domain-specific logic. This functionality enables the development of tailored stores that meet specific needs. Custom stores offer a high level of control and encapsulation, which supports efficient state management.
For instance, a custom store for a user's shopping cart, referred to as cart, can be created using a createCart function. This cart store maintains an array of items representing the user's selected products. It includes methods like addToCart, removeFromCart, and emptyCart to manage the cart's state. By subscribing to the cart store, other components can receive updates whenever changes occur, allowing them to adjust their rendering accordingly.
import { writable } from "svelte/store";
// Create a custom store for a shopping cart
function createCart() {
const { subscribe, set, update } = writable<Item[]>([]);
// Do not expose set and update methods
return {
subscribe,
addToCart: (item: Item) => update((cart: Item[]) => [...cart, item]),
removeFromCart: (id: number) =>
update((cart: Item[]) => cart.filter((item: Item) => item.id !== id)),
emptyCart: () => set([]),
};
}
// Singleton cart
export const cart = createCart();
export type Item = { id: number; name: string };
cart.ts
<script lang="ts">
import { onDestroy } from "svelte";
import { cart, type Item } from "./cart";
let items: Item[];
let id = 0;
const unsubscribe = cart.subscribe((val) => (items = val));
onDestroy(unsubscribe);
function addToCart() {
cart.addToCart({ id, name: `Item ${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 three 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 three buttons for cart updates. When changes occur in the cart store, Svelte detects them and re-renders the component. As a result, the list of items dynamically updates to reflect the latest state of the cart.
Slots
Slots are a mechanism that allows developers to create reusable and flexible components by enabling the insertion of content from a parent component into a child component. Named slots are especially useful for passing different content to specific areas within a component. By assigning names to slots, you can control which content appears in each slot, allowing for more precise component design. If a component does not receive content for a particular slot, you can implement slot fallbacks to ensure the component remains functional by providing alternative content. Slot props enable the passing of data from the parent component to the content within a slot, allowing for customization based on the parent's data, which enhances design flexibility. Additionally, conditional slots enable the selective rendering of different content sets within a component based on specific conditions, allowing for dynamic rendering or omission of various parts of the component's template.
<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 and target the parent element -->
<div class="description">
<slot />
</div>
<footer>
<span>Power:</span>
<slot name="power">100</slot>
</footer>
</div>
<style>
.card {
width: 20rem;
}
.description {
color: red;
}
header {
border-radius: 0.3em 0.3em 0 0;
}
footer {
border-radius: 0 0 0.3em 0.3em;
}
footer > span {
padding-right: 0.5em;
}
</style>
Card.svelte
<script lang="ts">
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>
App.svelte
In the code above, the Card component utilizes slots to allow for customization and injection of content. It offers two 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 withslot="name"
, will be assigned to the name slot. Similarly, the third span element, marked withslot="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 lang="ts">
let hovering: boolean;
const enter = () => (hovering = true);
const leave = () => (hovering = false);
</script>
<div on:mouseenter={enter} on:mouseleave={leave}>
<slot {hovering} />
</div>
<style>
div {
width: 10rem;
border: 1px solid green;
}
</style>
Hoverable.svelte
<script lang="ts">
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>
App.svelte
The Hoverable component enables tracking whether an element is being hovered over and communicates this status to its slotted content. It wraps the slotted content in a div element that listens for mouseenter and mouseleave events. When the mouse enters the element, the enter function is triggered, setting the hovering variable to true. On the other hand, when the mouse leaves, the leave function is triggered, changing hovering back to false.
In the slotted content, the
let:hovering={active}
syntax receives the hovering value from Hoverable. The active variable is used 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>
Profile.svelte
<script lang="ts">
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>
App.svelte
In this code example, the
$$slots
special variable is used 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 refers to 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 lang="ts">
import { getAllContexts, getContext, hasContext } from "svelte";
const name: string = hasContext("name") ? getContext("name") : "";
const { format }: { format: (name: string) => void } = getContext("format");
const contextMap = getAllContexts();
</script>
<p>{format(name)}</p>
<p>Contexts: {Array.from(contextMap.keys()).join(", ")}</p>
Child.svelte
<script lang="ts">
import Child from "./Child.svelte";
</script>
<Child />
Parent.svelte
<script lang="ts">
import Parent from "./Parent.svelte";
</script>
<Parent />
Grandparent.svelte
<script lang="ts">
import { setContext } from "svelte";
import Grandparent from "./Grandparent.svelte";
setContext("name", "Jonh Doe");
setContext("format", {
format: (name: string) => `Hello, ${name}`,
});
</script>
<Grandparent />
App.svelte
In the code above, the App 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 App 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 passing of information through various component levels.
<script lang="ts">
import { getContext, setContext } from "svelte";
import { writable, type Writable } from "svelte/store";
let name = writable("");
setContext("name", name);
let message: Writable<string> = 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>
This code example demonstrates 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>
element 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>
enables direct manipulation of the head element in components. It allows for dynamic modification or addition of elements such as title, style, or meta tags..
The <svelte:window>
, <svelte:body>
, and <svelte:document>
elements are used to define event listening on window, body, and document respectively within components. These elements enable the management of global events occurring outside the component's immediate scope, allowing for responses 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 enables you to adjust the behavior of your components by enabling or disabling specific features or optimizations.
export const data = [
{
name: "dir1",
files: [
{
name: "dir1A",
files: [{ name: "file.json" }],
},
],
},
{
name: "dir2",
files: [{ name: "file.txt" }],
},
{ name: "file.txt" },
];
data.ts
<script lang="ts">
export let name: string;
</script>
<span>{name}</span>
File.svelte
<script lang="ts">
import File from "./File.svelte";
export let name: string;
export let files: File[];
export let expanded: boolean;
type File = { name: string; files?: File[] };
</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 lang="ts">
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 the name, files, and expanded props 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. The File component displays 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>
AdminBadge.svelte
<span>Moderator</span>
ModeratorBadge.svelte
<span>User</span>
UserBadge.svelte
<script lang="ts">
import AdminBadge from "./AdminBadge.svelte";
import ModeratorBadge from "./ModeratorBadge.svelte";
import UserBadge from "./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>
Profile.svelte
In the code above, a Profile component and three 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 three identical components like in this demonstration.
<script lang="ts">
let show: boolean;
let key: string, selection: string;
</script>
<svelte:head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Gelasio"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/nes.css/2.3.0/css/nes.min.css"
/>
<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;
}
</style>
In this code example, the special elements
<svelte:head>
,<svelte:window />
,<svelte:body />
, and<svelte:document />
are used 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 lang="ts">
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>
App.svelte
In the code example above, the App 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. 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>
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 to detect direct value changes through referential equality checks. On the other hand, the default setting ofimmutable={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(): Promise<number[]> {
const data = [1, 2, 3, 4, 5];
return new Promise((resolve) => {
setTimeout(() => resolve(data), 2000);
});
}
api.ts
<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 lang="ts">
export let name: string;
setContext("name", name);
console.log(`Item: ${sharedFunction()}`);
</script>
{#each data as num}
<p>{num}</p>
{/each}
Item.svelte
<script lang="ts">
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 three instances of the Item component. Each Item component has two 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
Transitions and animations are both techniques used to create movement and visual effects in user interfaces, but they serve different purposes. Transitions are designed to change an element's style from one state to another in response to user interactions, such as hovering or clicking, and typically involve a smooth change between two states. In contrast, animations are more complex, allowing for multiple states and complex movements that can occur independently of user actions, often running automatically or in loops to create visual effects. Essentially, transitions emphasize smooth state changes, while animations offer a wider variety of dynamic and continuous visual experiences.
Svelte provides several modules that offer different functionalities for creating dynamic and animated experiences. One of these modules is svelte/motion
. It exports two functions, tweened and spring. These functions enable the creation of writable stores, which allow values to transition smoothly 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 seven 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 thirty one named exports, including a linear ease and three variants, in, out, and inOut, of ten 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 Techniques For Transitions
Transitions vital for 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 two keyframes to create the illusion of smooth animation. A keyframe refers to a specific point in a timeline of an animation that defines the starting or ending state. 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 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 adjust your animations.
<script lang="ts">
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 the tween completes
tweenProgress.update((n) => n + step);
}
}
function reset() {
progress = 0;
// Returns a promise that resolves when the tween 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>
progress {
flex: 1;
}
</style>
In the this code snippet, a tweened animation is implemented using the tweened store. 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 changes. 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 two approaches.
<script lang="ts">
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: number) => t, // Linear easing
interpolate: (from: number, to: number) => (t: number) =>
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>
<p style:--size={$tween + "px"}>
{$tween} x {$tween}
</p>
<style>
p {
width: var(--size);
height: var(--size);
background: red;
}
</style>
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 invoked, 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 speed 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 is crucial in defining how values change between the starting and ending points, enhancing the overall fluidity and visual attractiveness 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 lang="ts">
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>
p {
width: var(--size);
height: var(--size);
background: red;
}
</style>
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 inflexibility 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 movements, while a lower damping value enables more noticeable 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.
Applying Transitions
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 allows you to specify how an element should transition in and out of the DOM. You have various options to customize the animation, including parameters such as duration, easing, delay, and more.
The in: and out: properties of the transition directive are specifically used 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.
<script lang="ts">
import { fade, fly } from "svelte/transition";
let visible = true;
</script>
<label>
<input type="checkbox" bind:checked={visible} />
Make Visible
</label>
{#if visible}
<p transition:fade>Fades in and out</p>
<!--
"transition" directive with parameters.
The 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>
<!-- 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}
In the code example above, a boolean variables: visible is declared and set to true. This variable is bound to a checkbox input, allowing to toggle the visibility of the p elements.
The transitions fade and fly are utilized to animate the appearance and disappearance of elements when the checkbox is toggled. When the transition: directive is used, the same transition is applied when the element enters or exits the DOM. On the other hand, 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 offers transition events that allow you to track specific moments during the transition process, especially when an element is added to or removed from the DOM. These events can be utilized to trigger custom logic or perform actions based on the transition's state.
<script lang="ts">
import { fly } from "svelte/transition";
let visible = true;
let status: string;
</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}
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 simple method for animating CSS properties.
On the other hand, JavaScript transitions are animations created using JavaScript, providing more control over the animation logic. They enable the development of complex animations beyond the capabilities of CSS transitions.
CSS transitions are often preferred for their declarative nature, efficiency, and straightforward implementation using the transition directive. However, there are situations where custom JavaScript transitions are necessary. These transitions are advantageous for complex animations that cannot be accomplished with CSS transitions alone. JavaScript offers greater control and flexibility to dynamically manipulate elements and create complex animations. When transitions depend on dynamic data or user interactions, custom JavaScript transitions are ideal, as they enable responsiveness to events, real-time updates to animation properties, and transitions based on specific conditions. Additionally, for advanced or custom timing functions, such as elastic or bounce effects, JavaScript allows you to define and effectively integrate these into your animations.
<script lang="ts">
import { elasticOut } from "svelte/easing";
let visible = true;
function fade(node: Element, { delay = 0, duration = 400 }) {
const opacity = +getComputedStyle(node).opacity;
return {
delay,
duration,
css: (t: number) => `opacity: ${t * opacity};`,
};
}
function spin(node: Element, { duration }: { duration: number }) {
return {
duration,
css: (t: number) => {
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 animation
In this code example, there are two 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 lang="ts">
let visible = false;
function typewriter(node: Element, { 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: number) => {
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 animation
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.
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 For Triggering Transitions
Key blocks are utilized to trigger transitions when the value of an expression changes. They allow for dynamic updates to elements, ensuring that transitions happen when values change, rather than when elements are added to or removed from the DOM.
<script lang="ts">
import { onMount } from "svelte";
let i = 0;
onMount(() => {
const interval = setInterval(() => i++, 5000);
return () => clearInterval(interval);
});
function typewriter(node: Element, { 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>
In this code example, the
{#key}
block is utilized to create a typewriter effect on the p element. By using the{#key i}
block, Svelte recognizes that the element should be re-rendered whenever the value ofi
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, ignoring changes ini
, and thus preventing the typewriter animation from occurring.
Transition Modifiers
In Svelte 4+, 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 two 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 two 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 lang="ts">
import { slide } from "svelte/transition";
let items = ["Items 1", "Items 2", "Items 3"];
let visible = true;
function addItem() {
items = [...items, `Item ${items.length + 1}`];
}
</script>
<button on:click={addItem}>Add Item</button>
<label>
<input type="checkbox" bind:checked={visible} />
Make Visible
</label>
{#if visible}
{#each items as item}
<!--
In Svelte 4, transition applies to a single p element.
In Svelte 3, transition applies to each p element.
To achieve the Svelte 4 behavior in Svelte 3, use "transition:slide|local".
-->
<p transition:slide>{item}</p>
{/each}
{#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}
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 includes a useful feature called deferred transitions, which enables synchronization between several elements. For example, consider two todo lists where moving a todo item from one list to the other creates a fluid animation. Instead of items suddenly vanishing and reappearing, they transition through a range of intermediate positions, resulting in a smoother visual experience.
To achieve this effect, Svelte provides the crossfade function. This function generates two transitions called send and receive. When an element is "sent", it smoothly transforms to its counterpart's position and fades out. On the other hand, when an element is "received", it smoothly moves to its new position and fades in.
import { writable } from "svelte/store";
export type Todo = { id: number; done: boolean; description: string };
export function createTodoStore(initial: Todo[]) {
let uid = 1;
const todos = initial.map(({ done, description }) => {
return {
id: uid++,
done,
description,
};
});
const { subscribe, update } = writable(todos);
return {
subscribe,
add: (description: string) => {
const todo = {
id: uid++,
done: false,
description,
};
update(($todos) => [...$todos, todo]);
},
remove: (todo: Todo) => {
update(($todos) => $todos.filter((t: Todo) => t !== todo));
},
mark: (todo: Todo, done: boolean) => {
update(($todos) => [
...$todos.filter((t: Todo) => t !== todo),
{ ...todo, done },
]);
},
};
}
todos.ts
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.ts
<script lang="ts">
import { send, receive } from "./transition.js";
import { createTodoStore } from './todos.js';
export let store: ReturnType<typeof createTodoStore>;
export let done: boolean;
</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>
TodoList.svelte
<script lang="ts">
import { createTodoStore, type Todo } from "./todos";
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" },
] as Todo[]);
</script>
<input
placeholder="What needs to be done?"
on:keydown={(e) => {
if (e.key !== "Enter") return;
todos.add(e.currentTarget.value);
e.currentTarget.value = "";
}}
/>
<h3>Todo</h3>
<TodoList store={todos} done={false} />
<h3>Done</h3>
<TodoList store={todos} done={true} />
App.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 used in handling the transition of a todo item from one list to another. When invoked, the send function accepts two 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 three 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 provides a visually appealing effect as items move between the Todo and Done.
Animate Directive
To create a smooth experience and improve the overall effect, it is important to introduce motion into elements that are not currently transitioning. This can be achieved with the animate directive. Svelte provides a flip animation function, which is useful when you want to animate a list of items that are being reordered. The animate:
directive is essential for applying these animations to the elements.
import { writable } from "svelte/store";
export type Todo = { id: number; done: boolean; description: string };
export function createTodoStore(initial: Todo[]) {
let uid = 1;
const todos = initial.map(({ done, description }) => {
return {
id: uid++,
done,
description,
};
});
const { subscribe, update } = writable(todos);
return {
subscribe,
add: (description: string) => {
const todo = {
id: uid++,
done: false,
description,
};
update(($todos) => [...$todos, todo]);
},
remove: (todo: Todo) => {
update(($todos) => $todos.filter((t: Todo) => t !== todo));
},
mark: (todo: Todo, done: boolean) => {
update(($todos) => [
...$todos.filter((t: Todo) => t !== todo),
{ ...todo, done },
]);
},
};
}
todos.ts
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.ts
<script lang="ts">
import { flip } from "svelte/animate";
import { send, receive } from "./transition";
import { createTodoStore } from "./todos";
export let store: ReturnType<typeof createTodoStore>;
export let done: boolean;
</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>
TodoList.svelte
<script lang="ts">
import { createTodoStore, type Todo } from "./todos";
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" },
] as Todo[]);
</script>
<input
placeholder="What needs to be done?"
on:keydown={(e) => {
if (e.key !== "Enter") return;
todos.add(e.currentTarget.value);
e.currentTarget.value = "";
}}
/>
<h3>Todo</h3>
<TodoList store={todos} done={false} />
<h3>Done</h3>
<TodoList store={todos} done={true} />
App.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 smooth 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 useful when responding to user interactions and animating elements accordingly.
Actions
Actions are a method that enable direct interaction with the DOM or manages side effects within components. They are typically used for tasks like manipulating elements, managing events, or integrating with external libraries. While actions are not widely used, they provide a way to execute a function when an element is added to the DOM. Essentially, actions can be viewed as lifecycle hooks for individual DOM elements, similar to onMount or onDestroy, but specifically associated with the elements rather than the entire components.
To define an action, you can provide the DOM element as a parameter, along with any optional parameters needed. The action function can perform initialization logic on the element and may return an object that includes update and destroy handlers. These handlers assist in managing any updates or cleanup tasks associated with the action.
To apply an action to a specific element, the use:
directive is utilized.
export function resizeAction(node: HTMLDivElement) {
// 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: string) => {
// 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.ts
<script lang="ts">
import { resizeAction } from "./actions";
let visible = true;
let bgColor: string;
</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: 1px solid red;
resize: both;
overflow: auto;
}
</style>
App.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, 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 provide developers with robust tools to improve their CSS workflow.
SCSS/SASS offer features such as variables, nesting, mixins, and functions, which enhance the maintainability of CSS code. LESS, another widely used preprocessor, provides similar functionalities to SCSS/SASS but with a slightly different syntax.
Stylus is recognized for its minimalistic approach, emphasizing simplicity and flexibility, and utilizes an indentation-based syntax for clear and expressive coding.
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 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
foo@bar:~$ npm install -D sass
# LESS
foo@bar:~$ npm install -D less
# Stylus:
foo@bar:~$ npm install -D stylus
Install a CSS preprocessor
<script lang="ts">
// 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>
TailwindCSS 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 enable you to efficiently develop custom designs without being constrained by generic prebuilt components. To integrate TailwindCSS into a Svelte project, start by running npm install -D tailwindcss postcss autoprefixer
in the terminal. Alternatively, you can use npx svelte-add@latest tailwindcss
, which installs all three packages and sets up TailwindCSS if you prefer that approach.
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 TailwindCSS into a project without utilizing the Tailwind CLI. Given that TailwindCSS 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 TailwindCSS by running npx tailwindcss init -p
in the terminal. This command initializes TailwindCSS and generates a tailwind.config.js file along with a postcss.config.js, allowing you to customize Tailwind's and PostCSS's configuration settings. If you have used svelte-add, ignore this step as TailwindCSS is already initialized.
In the tailwind.config.js file, add the content
option to eliminate unused CSS by specifying the relevant files.
...
content: ['./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 TailwindCSS, you can use the TailwindCSS 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 or 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.ts
svelte-add is a CLI tool designed to simplify the process of adding features and functionalities to Svelte applications. It provides a collection of community-contributed add-ons that can be easily integrated into Svelte projects. These add-ons include various functionalities, such as routing, styling, and testing solutions, allowing developers to enhance their applications without having to set everything up from scratch.
Shadcn Integration
Shadcn is a set of beautifully designed components that are accessible, customizable, and open source, allowing you to easily copy and paste them into your applications. It is important to note that this is not a traditional component library, rather, it is a collection of reusable components that you do not install as a dependency, as it is not available or distributed via npm. You can select the components you need, copy and paste the code into your project, and customize it to fit your requirements, giving you full ownership of the code. This approach keeps the design of your components separate from their implementation.
shadcn-svelte offers a collection of reusable components developed with Bits UI, Melt UI, and TailwindCSS as a community-driven Svelte adaptation of shadcn/ui. This collection differs from traditional component libraries in that it is not available through npm and is not intended for installation as a dependency. Instead, users have the flexibility to copy and paste the code directly into their projects or utilize the CLI for integration, allowing for full customization and ownership of the code. The CLI will generate a folder for each component, which may contain a single Svelte file or multiple files. Each folder will include an index.ts file that exports the components, allowing for easy imports from a single file. This project and its components are written in TypeScript, and while it is recommended to use TypeScript for your project, a JavaScript version of the components is also available through the CLI.
To install shadcn-svelte, begin by installing TailwindCSS by running the command npx svelte-add tailwindcss
. Next, update your path aliases in your tsconfig.json and vite.config.ts for TypeScript projects, or in your jsconfig.json and vite.config.js for JavaScript projects. Finally, you can install shadcn-svelte by executing npx shadcn-svelte@latest init
. To add a specific component to your project, run npx shadcn-svelte@latest add <component>
. Replace with the desired component. Once completed, you are ready to start using it. You can find all components, examples, theming, and typography on shadcn-svelte.com.
foo@bar:~$ npx svelte-add tailwindcss
foo@bar:~$ # At this point, update configuration files
foo@bar:~$ npx shadcn-svelte@latest init
foo@bar:~$ npx shadcn-svelte@latest add button
{
...
"compilerOptions": {
"baseUrl": ".",
"paths": {
"$lib": ["./src/lib"],
"$lib/*": ["./src/lib/*"]
},
...
}
...
}
tsconfig.json or jsconfig.json
import path from "path";
export default defineConfig({
...
resolve: {
alias: {
$lib: path.resolve("./src/lib"),
},
},
});
vite.config.ts or vite.config.js
Flowbite Integration
Flowbite is an open-source collection of UI components created using utility classes from TailwindCSS, acting as a basis for creating user interfaces and websites. Flowbite Svelte builds on this by offering an open-source UI component library that includes native Svelte components and their interactive capabilities, improving the core Flowbite components. This library provides a range of interactive elements, including navbars, dropdowns, modals, and sidebars, all developed with Svelte and using TailwindCSS utility classes. These components also support accessibility properties and theming. The library allows for customization, enabling you to modify the primary color by altering the specified color values in the provided code.
To install Flowbite Svelte, start by setting up TailwindCSS by executing the command npx svelte-add tailwindcss
. Then, run npm i -D flowbite-svelte flowbite
to install Flowbite and its dependencies. Additionally, you will need to update the tailwind.config.js file in your project to instruct the TailwindCSS compiler on where to locate the utility classes and to set up the Flowbite plugin. Once completed, you are ready to start using it. You can find all components, examples, and theming on flowbite-svelte.com.
foo@bar:~$ npx svelte-add tailwindcss
foo@bar:~$ npm i -D flowbite-svelte flowbite
...
content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'],
...
tailwind.config.js
Icons
Iconify is a tool designed for developers to use a broad selection of icons from multiple icon sets within their applications. It offers a unified API that enables access to icons from various libraries, simplifying the process of implementing icons without the need to handle several libraries individually. Iconify provides native icon components for several widely-used UI frameworks. One of these is Iconify for Svelte, which loads icons on demand. This means there is no need to bundle icons. The component will automatically retrieve icon data for the icons you use from the Iconify API.
To get started with Iconify, you can install the package by running npm install -D @iconify/svelte
. After the installation, import the Icon component into your components and pass the neseccary props to it. The only required prop is the icon, which can be an IconifyIcon or a string representing the icon name or data. Optional props include inline for vertical alignment, width and height to set dimensions, hFlip and vFlip for horizontal and vertical flipping, flip as an alternative to the flip properties, rotate for rotation, color to change the icon's color, and onLoad as a callback when the icon data is loaded. Additionally, the icon component accepts any other props that will be applied to the generated SVG element, allowing for inline styles and titles. Note that Icon does not support events.
<script lang="ts">
import Icon from "@iconify/svelte";
</script>
<p class="skills">Skills:</p>
<div class="container">
<Icon width="48" height="48" icon="skill-icons:html" />
<Icon width="48" height="48" icon="skill-icons:css" />
<Icon width="48" height="48" icon="skill-icons:javascript" />
<Icon width="48" height="48" icon="skill-icons:typescript" />
<Icon width="48" height="48" icon="skill-icons:svelte" />
</div>
<style>
.skills {
font-weight: bold;
font-size: 1.5rem;
text-align: left;
margin-bottom: 1rem;
}
.container {
display: flex;
gap: 1em;
}
</style>
The example code above displays five Icon components, each with defined width and height attributes, both set to 48, which determines the size of the icons. Each icon is identified by its name, such as "skill-icons:html", representing the "html" icon from the "skill-icons" set. You can view all icon sets and individual icons by visiting icon-sets.iconify.design
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 way to implement lazy loading is through the Intersection Observer API, which enables you to react to the intersection between the browser's viewport and an HTML element. The viewport is the visible area of a web page that the user can see without scrolling or zooming. By identifying when a user is about to engage with a resource, you can efficiently implement lazy loading.
<script lang="ts">
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>
LazyLoader.svelte
<script lang="ts">
import { onMount } from "svelte";
let message: string;
onMount(() => {
setTimeout(() => (message = "Hello, world"), 3000);
});
</script>
<p>{message ?? "Loading..."}</p>
Component.svelte
<script lang="ts">
import Component from "./Component.svelte";
import LazyLoader from "./LazyLoader.svelte";
</script>
<!-- Add a substantial amount of text to display in order to create a scrollable page -->
<LazyLoader>
<Component />
</LazyLoader>
<LazyLoader>
<img alt="" src="https://picsum.photos/200" />
</LazyLoader>
App.svelte
In the code example above, three components are defined, LazyLoader, Component, and App, with App serving as the entry point. The LazyLoader component utilizes the Intersection Observer API to implement lazy loading and determine when its content should be shown. The Component renders a delayed message inside a <p>
element after a three second wait. The App component functions as the main component, importing the other two components and managing the lazy loading of both the Component and an image within LazyLoader.
This configuration ensures that components and images are loaded only when they are about to become visible in the viewport. The message in the Component will appear after a three second delay once it is visible on the page.
Consuming APIs
Consuming RESTful and GraphQL APIs provides a range of options. For RESTful APIs, tools like the Fetch API or HTTP clients such as Axios are frequently used to handle HTTP requests. For GraphQL APIs, you can utilize the Fetch API or the Apollo Client library. If you decide to use Axios or Apollo Client, setting them up is as simple as one npm install and an import.
When working with APIs, you can integrate API calls within the onMount lifecycle function to enable data retrieval when the component loads. Alternatively, using module context allows access to data that can be shared across all instances of the component. Using {#await}
blocks is an effective way to manage asynchronous operations. Additionally, triggering API requests through event handlers offers a user-centric approach to fetching data. To ensure state persistence across component remounts and maintain efficient data management processes, it is beneficial to store API responses in a store.
REST stands for Representational State Transfer. It is an architectural style for designing networked applications. It relies on a stateless, client-server communication model that uses standard HTTP methods such as GET, POST, PUT, and DELETE to perform operations on resources, which are typically represented in formats like JSON or XML. RESTful refers to services or APIs that comply with those principles and constraints. In other words, a RESTful API is one that implements the REST architecture, ensuring that it follows the guidelines such as statelessness, resource representation, and the use of standard HTTP methods.
On the other hand, GraphQL is a query language for APIs and a runtime for executing those queries. It typically uses HTTP requests to communicate between clients and servers, just like REST. However, it operates differently in how data is requested and returned. While REST APIs often require multiple endpoints for different resources, GraphQL uses a single endpoint to handle all queries. Clients can specify exactly what data they need in their requests. This flexibility can lead to more efficient data retrieval and reduce over-fetching or under-fetching of data.
In summary, RESTful APIs focus on a resource-oriented approach with multiple endpoints, while GraphQL provides a more flexible and efficient way to query data through a single endpoint.
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 Routify or by manually managing the application state based on the URL.
Routify is a routing framework designed specifically for Svelte applications, providing a powerful and flexible way to manage navigation and routing within SPAs. It offers a file-based routing system, which allows developers to define routes based on the file structure of their project. Key features of Routify include support for dynamic routing, nested routes, and route parameters. . Additionally, Routify includes built-in support for code splitting, which ensures that only the necessary code is loaded for each route, potentially improving application performance, server-side rendering (SSR), and prerendering.
To implement routing with Routify, begin by executing npm init routify@latest
to install the framework. During the installation, you will be prompted to choose whether you want to include examples. Alternatively, you can install it by running npx svelte-add routify
. Once the installation is complete, you can start defining your routes. In Routify, the files and folders within the src/routes directory are automatically mapped to corresponding routes. For example, both src/routes/component.svelte and src/routes/component/index.svelte will result in the same URL, /component. Routify is compatible with native <a>
anchors and the history.pushState and history.replaceState methods, but it does not intercept links to external domains, anchors with a target attribute, or links outside of Routify's page or modules. For programmatic navigation, you can use history.pushStateNative and history.replaceStateNative. Parameters can be included as plain URL queries, such as /posts?page=1, and accessed using the $params helper. To create more aesthetically pleasing URLs, dynamic pages can be used, like /posts/[page].svelte, which allows for cleaner URLs such as /posts/1. Variadic parameters (variable number of parameters) can be managed with the spread operators, allowing for multiple arguments.
In Routify, every file and folder functions as a node within a route tree, enabling both navigation and the retrieval of metadata. Router hooks can be used through $ helpers or directly on the router, allowing for the management of different stages in the routing process. Preloading can take place within the module script before the component loading, which is advantageous for tasks such as data fetching. Files are dynamically imported by default to improve load times, and bundling options are available if necessary. The Inline feature enables the simultaneous rendering of multiple pages, enhancing the user experience with smooth navigation. Additionally, Routify supports hash-based navigation and can be rendered on the client side (CSR), server side (SSR), or via pre-rendering (SSG). For additional information, you can check the Routify API documentation.
<script lang="ts">
function navigate(page: string) {
window.location.href = `/${page}`;
}
</script>
<p>Home</p>
<button on:click={() => navigate("register")}>Register</button>
<button on:click={() => navigate("login")}>Login</button>
Home.svelte
<p>Register</p>
Register.svelte
<p>Login</p>
Login.svelte
<script lang="ts">
function navigate() {
window.location.href = "/";
}
</script>
<p>404 - Page Not Found</p>
<button on:click={navigate}>Go to Home</button>
404.svelte
<script lang="ts">
import { onMount } from "svelte";
export let useHash = false;
export let routes: Record<string, ConstructorOfATypedSvelteComponent> = {};
let currentRoute = "";
function handleRoute() {
const { hash, pathname } = window.location;
currentRoute = (useHash ? hash?.slice(1) : pathname?.slice(1)) || "/";
}
onMount(() => {
handleRoute();
useHash && window.addEventListener("hashchange", handleRoute);
});
$: Component = routes[currentRoute] || routes["*"];
</script>
<svelte:component this={Component} />
Router.svelte
<script lang="ts">
import Router from "./Router.svelte";
import Error404 from "./404.svelte";
import Home from "./Home.svelte";
import Register from "./Register.svelte";
import Login from "./Login.svelte";
const routes = {
"/": Home,
register: Register,
login: Login,
"*": Error404,
};
</script>
<Router {routes} useHash={false} />
App.svelte
In the code example provided, a very simple Single Page Application (SPA) is developed using a custom router component to enable navigation between different components. The Router component establishes the routing logic for the application, mapping various routes to specific components such as Home, Register, Login, and Error404, which are provided through the routes prop. The current route is determined by either the hash segment of the URL or the pathname. For hash-based routing, a listener is implemented to detect changes in the hash. The useHash prop is used to configure the routing type, indicating whether hash-based routing is used or not. If the specified route does not exist, the 404 component is displayed.
In summary, this example demonstrates basic client-side routing within a Svelte application. It is important to note that this code requires refinements to be ready for production use.
There are two main approaches to rendering web applications, server-side rendering (SSR) and client-side rendering (CSR). SSR involves rendering the application on the server and sending the pre-rendered HTML to the client. This improves initial load time and search engine optimization (SEO). In SSR, the server handles both rendering and initial state management. On the other hand, CSR involves rendering the application on the client-side using JavaScript. This allows for more dynamic and interactive experiences, as the application can respond to user interactions without making additional requests to the server. However, CSR may have a slower initial load time and potential SEO challenges if not implemented properly. Note that some components may not be suitable for SSR if they rely on browser-specific features. In such cases, CSR may be the preferred option.
To bridge the gap between SSR and CSR, a concept called hydration is used. Hydration is the process of taking the pre-rendered HTML sent by the server and attaching event listeners and interactivity to it on the client-side. This allows the application to become fully interactive without making additional requests to the server. Hydration is a crucial step in transitioning from the initial static HTML to a dynamic client-side application.
Pre-rendering is another technique, that combines aspects of CSR and SSR. During the build process, static HTML pages are generated, like SSR. However, unlike SSR where the server handles subsequent interactivity, pre-rendering generates HTML that is already interactive. This means that the generated HTML includes the necessary JavaScript code to handle user interactions without relying on additional requests to the server. Pre-rendering provides the benefits of pre-rendered HTML while still allowing for interactivity. It can be applied for static site generation (SSG) to construct a website where every page is pre-rendered.
In summary, CSR involves the browser using JavaScript to generate HTML content, resulting in the server sending a minimal HTML file while the browser dynamically builds the page. On the other hand, SSR and pre-rendering create the HTML on the server, delivering a fully rendered page to the client. Both SSR and pre-rendering generate HTML before it reaches the client, but they differ in execution. Pre-rendering occurs at build time, producing static HTML pages for each route, which means the content is ready to be served as static files without requiring server rendering for each request. However, SSR takes place at runtime, with the server generating HTML in response to each request, allowing for dynamic content. Pre-rendering focuses on creating static content, while hydration is a technique that primarily applies to SSR and involves adding interactivity to that content.
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 smooth user experience.
import { writable } from "svelte/store";
export type User = { email?: string };
export const isLoggedIn = writable(false);
export const userDetails = writable<User>({});
user.ts
<script lang="ts">
import { isLoggedIn, userDetails, type User } from "./user";
const values: User = {};
let errors: User = {};
async function register(data: User) {
// Emulate sending data to server
return new Promise((resolve) => {
setTimeout(() => {
isLoggedIn.set(true);
userDetails.set(data);
resolve(undefined);
}, 1000);
});
}
async function submitHandler() {
// Implement validation logic with 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}>
<h3>Register</h3>
<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>
Register.svelte
<script lang="ts">
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 lang="ts">
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 demonstrates a simple user authentication system within a Svelte application. In the App component, which serves as the main component, users are routed to either the Profile or Register component based on their login status. The Register component handles a form that validates user inputs and simulates the registration process. The Profile component shows the user's email and includes an option to log out. The user.ts file includes Svelte writable stores for monitoring login status and storing user details. Note that the validation process should be implemented with a validation library like Yup or Zod.
Documenting
You can document your components by utilizing specially formatted comments in editors that support the Svelte Language Server. Within the markup section of a component, you can use the @component
tag inside an HTML comment to describe the component. This documentation will appear when you hover over the component element. You can also use Markdown formatting within the documentation comment to provide clear and structured information about the component, including usage examples and code snippets. By following this format, you can effectively document your Svelte components, improving their clarity and ease of maintenance.
The Svelte Language Server is a tool aimed at improving the development experience for Svelte applications in code editors. It offers features such as syntax highlighting, IntelliSense for code completion, error checking, and hover information that presents relevant documentation or type details when hovering over components.
User.svelte - Providing a screenshot because the three backticks disrupt the Markdown renderer on Dev.to
<script lang="ts">
import User from "./User.svelte";
</script>
<!-- Hover over "User" -->
<User username='admin' />
App.svelte
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. Moreover, reactive statements can be employed to log value changes. 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 lang="ts">
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}
Formatting & Linting
Formatting and linting are essential aspects of maintaining code quality in software development. Formatting involves ensuring that code follows a consistent style, which includes elements such as indentation, spacing, line length, and bracket placement. The primary goal is to enhance the readability and maintainability of the code. On the other hand, linting is the process of analyzing code to identify potential errors, bugs, or stylistic issues. Linting tools can detect problems that may not prevent the code from executing but could lead to bugs or suboptimal coding practices.
In the JavaScript ecosystem, ESLint and Prettier are widely used tools. ESLint is a static code analysis tool designed to identify problematic patterns in JavaScript code. It helps developers in enforcing coding standards and catching errors before they escalate into production issues. Prettier is a code formatter that emphasizes the style and formatting of code. It automatically formats your code to ensure a consistent appearance, regardless of the author. Prettier takes care of aspects such as indentation, spacing, and line breaks, allowing developers to concentrate on writing code without the distraction of formatting concerns. These tools can be effectively integrated into a Svelte application to enhance both code quality and developer productivity.
Another set of tools that can be integrated into this stack are Husky and lint-staged. Husky is a command-line interface (CLI) tool that simplifies the use of Git hooks. It allows you to install Git hooks that can be linked to various commands. For example, you can use the pre-commit Git hook to trigger a command, which can then determine whether to allow or disallow a commit based on the outcome. lint-staged complements this by targeting only the files that have changed. While it might seem unnecessary at first, as your project expands in size, you will likely find this feature to be quite valuable.
To set up ESLint and Prettier in your project, start by installing the necessary packages. For JavaScript Svelte projects, run npm install -D eslint eslint-config-prettier eslint-plugin-svelte prettier prettier-plugin-svelte
. If you are working with TypeScript, include the following additional packages by running npm install -D typescript-eslint @typescript-eslint/parser typescript-eslint-parser-for-extra-files svelte2tsx
. Next, in the root directory, create the configuration files eslint.config.js, .prettierrc, and .prettierignore to define your linting and formatting rules. Add the content provided below and make any necessary adjustments based on your project's requirements. Once the configurations are in place, you can format your code and fix linting issues by running npx prettier --write src
followed by npx eslint --fix src
. To streamline these tasks, add them to the scripts section of package.json
foo@bar:~$ npm install -D eslint eslint-config-prettier eslint-plugin-svelte prettier prettier-plugin-svelte
Setup on a JavaScript Svelte project
foo@bar:~$ npm install -D eslint eslint-config-prettier eslint-plugin-svelte prettier prettier-plugin-svelte
foo@bar:~$ npm install -D typescript-eslint @typescript-eslint/parser typescript-eslint-parser-for-extra-files svelte2tsx
Setup on a TypeScript Svelte project
import globals from 'globals';
import pluginJs from '@eslint/js';
import svelteEslint from 'eslint-plugin-svelte';
export default [
{
files: ['**/*.{js,svelte}']
},
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: globals.browser,
}
},
pluginJs.configs.recommended,
...svelteEslint.configs['flat/recommended'],
{
ignores: ['.DS_Store', 'node_modules/*', 'dist/*', '**/.env', 'package-lock.json']
}
];
eslint.config.js for JavaScript Svelte project
import globals from 'globals';
import pluginJs from '@eslint/js';
import tsEslint from 'typescript-eslint';
import svelteEslint from 'eslint-plugin-svelte';
export default [
{
files: ['**/*.{js,ts,svelte}']
},
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: globals.browser,
parser: 'svelte-eslint-parser',
parserOptions: {
parser: 'typescript-eslint-parser-for-extra-files',
project: './tsconfig.json'
}
}
},
pluginJs.configs.recommended,
...tsEslint.configs.recommended,
...svelteEslint.configs['flat/recommended'],
{
ignores: ['.DS_Store', 'node_modules/*', 'dist/*', '**/.env', 'package-lock.json']
}
];
eslint.config.js for TypeScript Svelte project
{
"arrowParens": "avoid",
"bracketSpacing": false,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"semi": true,
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
.prettierrc for JavaScript or TypeScript Svelte projects
.DS_Store
node_modules
/dist
.env
package-lock.json
.prettierignore for JavaScript or TypeScript Svelte projects
...
"scripts": {
...
"lint": "eslint --fix src",
"format": "prettier --write src"
}
...
package.json for JavaScript or TypeScript Svelte projects
foo@bar:~$ npm run format
foo@bar:~$ npm run lint
# OR
foo@bar:~$ npx prettier --write src
foo@bar:~$ npx eslint --fix src
Run ESLint & Prettier
foo@bar:~$ npm install -D husky lint-staged
foo@bar:~$ npx husky init
Setup Husky & lint-staged on JavaScript or TypeScript Svelte projects
After running these commands, Husky should have set up a pre-commit hook in the .husky directory. Open that file in your code editor and append the npx lint-staged
command at the bottom. Whenever you attempt to commit your changes in Git, Husky will trigger the lint-staged command, which can then execute other commands only on the files affected by the commit. To configure lint-staged, you can add a new script in the scripts section of your package.json, such as "lint-staged": "svelte-check && npm run lint"
. This setup will run the specified commands on the files that are committed. If any of the checks fail, the commit will be aborted. svelte-check is a tool that provides command-line diagnostics to help developers identify issues in their Svelte projects. It checks for unused CSS, offers accessibility hints, and detects JavaScript and TypeScript compilation errors, ensuring that your code is both efficient and accessible.
...
"scripts": {
...
"lint-staged": "svelte-check && npm run lint"
}
...
Git hooks are scripts that Git runs automatically before or after specific events like committing, pushing, and pulling. They enable you to automate various tasks and enforce policies, allowing you to customize your Git workflow to better suit your project's needs.
Storybook
Storybook is an open-source tool that allows developers to create and demonstrate UI components in isolation. It enables developers to build components independently from the main application, simplifying the processes of building, testing, and documenting UI elements. Users can visualize and interact with different states of each component to ensure they work as intended. Storybook supports multiple frameworks, including React, Vue, Angular, and Svelte. It features a zero-configuration setup with built-in TypeScript support, making it easy to get started. Additionally, it includes a Svelte-native story format, as well as auto-generated controls and documentation, designed to enhance and simplify the development experience.
To get started, run npx storybook init
or npx sb init
in the root directory of your existing Svelte project. This command will detect your project type, install @storybook/svelte, and create sample files to demonstrate the fundamentals of Storybook. A story is essentially a code snippet that displays a component in a specific state. Storybook supports a native story format for Svelte, allowing you to write your stories using the familiar Svelte syntax. To enable this, install addon-svelte-csf by running npm install -D @storybook/addon-svelte-csf
.
You can configure Storybook in the .storybook/main.js or .storybook/main.ts file. By default, it searches for stories in the src directory, but you have the flexibility to store them in any location, and under any directory structure, you prefer. When you run npm run storybook
, a page will open in your browser where you can browse your stories. Storybook also supports hot-reloading, which means you do not need to restart it after updating a component or story file.
foo@bar:~$ npx storybook init
foo@bar:~$ npm install -D @storybook/addon-svelte-csf
foo@bar:~$ npm run storybook
Install and run Storybook
<script lang="ts">
export let isPrimary = false;
export let backgroundColor: string = "";
export let size: "small" | "medium" | "large" = "medium";
export let label: string = "";
$: mode = isPrimary
? "button--primary"
: "button--secondary";
$: style = backgroundColor ? `background-color: ${backgroundColor}` : "";
</script>
<button
type="button"
class={["button", `button--${size}`, mode].join(" ")}
{style}
on:click
>
{label}
</button>
<style>
.button {
display: inline-block;
cursor: pointer;
border: 0;
border-radius: 3em;
}
.button--primary {
background-color: #1ea7fd;
color: white;
}
.button--secondary {
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
background-color: transparent;
color: #333;
}
.button--small {
padding: 10px 16px;
font-size: 12px;
}
.button--medium {
padding: 11px 20px;
font-size: 14px;
}
.button--large {
padding: 12px 24px;
font-size: 16px;
}
</style>
Button.svelte
import type { Meta, StoryObj } from "@storybook/svelte";
import Button from "./Button.svelte";
const meta = {
title: "Example/Button",
component: Button,
tags: ["autodocs"],
argTypes: {
backgroundColor: { control: "color" },
size: {
control: { type: "select" },
options: ["small", "medium", "large"],
},
},
} satisfies Meta<Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
isPrimary: true,
label: "Click",
},
};
export const Secondary: Story = {
args: {
label: "Click",
},
};
export const Large: Story = {
args: {
size: "large",
label: "Click",
},
};
export const Small: Story = {
args: {
size: "small",
label: "Click",
},
};
Button.stories.ts without addon-svelte-csf - CSF syntax
<script lang="ts">
import { Meta, Story, Template } from "@storybook/addon-svelte-csf";
import Button from "./Button.svelte";
</script>
<Meta
title="Example/Button"
component={Button}
tags={["autodocs"]}
argTypes={{
backgroundColor: { control: "color" },
size: {
control: { type: "select" },
options: ["small", "medium", "large"],
},
}}
/>
<Template let:args>
<Button {...args} />
</Template>
<Story
name="Primary"
args={{
isPrimary: true,
label: "Click",
}}
/>
<Story
name="Secondary"
args={{
label: "Click",
}}
/>
<Story
name="Large"
args={{
size: "large",
label: "Click",
}}
/>
<Story
name="Small"
args={{
size: "small",
label: "Click",
}}
/>
Button.stories.svelte using addon-svelte-csf - SCF syntax
CSF, which stands for Component Story Format, is a standard used in Storybook for creating stories, enabling developers to create portable and reusable examples through a JavaScript object-based syntax. On the other hand, SFC, or Single File Component, is a format unique to Svelte that combines a component's template, logic, and styles into a single file, typically with a .svelte extension.
Testing
In a Svelte application, there are typically three 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, Vitest is a great modern option. It is a testing framework built on Vite, designed for rapid unit testing in JavaScript and TypeScript applications. It provides a fast and efficient testing environment by leveraging Vite's development server, enabling instant feedback during the testing process. Key features of Vitest include support for a wide range of assertions, mocking capabilities, and a powerful watch mode that automatically reruns tests on file changes.
To begin unit testing, first install Vitest by running the command npm i -D vitest
. After installation, you can start writing your tests. Vitest uses a syntax that is similar to Jest or Chai, and you can find more information about the syntax at Vitest API documentation. Once you have written your tests, you can run them by executing npx vitest run
. Vitest will automatically search for and execute files in your project that have the extensions .test.js, .test.ts, .spec.js, or .spec.ts. You can organize these files in any location you prefer.
For component testing, it is recommended to combine Vitest with svelte-testing-library, which provides utilities designed to test Svelte components in a way that reflects user interactions. svelte-testing-library is a testing utility designed specifically for Svelte applications, built on top of the Testing Library principles. It provides a simple and effective way to test Svelte components. Key features of svelte-testing-library include support for rendering Svelte components in a test environment, querying elements, and simulating user events to ensure components behave as expected. It encourages developers to write tests that reflect how users interact with the application. Additionally, it integrates with popular testing frameworks, enhancing the overall testing experience and ensuring the quality and reliability of Svelte applications. In this scenario, component tests are executed using Vitest. You can find more information about the syntax at Svelte Testing Library API documentation.
To start component testing, first install Vitest as mentioned in the unit testing paragraph above. Next, run the command npm i -D @testing-library/svelte jsdom
to install the svelte-testing-library and a DOM environment for rendering your components. After that, update your vitest.config.ts file to specify the DOM implementation that your tests will use. Then,, you can write your component tests and execute them by running npx vitest run
. Vitest will automatically search for and execute files in your project that have the extensions .test.js, .test.ts, .spec.js, or .spec.ts. You can organize these files in any location you prefer.
Coverage reporting is a measurement that indicates how much of your code is tested by your test suite, helping you understand which parts of your codebase are covered by tests and which parts are not. It provides metrics such as line coverage, branch coverage, and function coverage. This information is crucial for identifying untested areas, ensuring code quality, and improving the reliability of your application. To implement coverage reporting with Vitest, you can install the necessary package by running npm i -D @vitest/coverage-v8
, and then execute your tests with coverage using npx vitest run --coverage
.
If you are interested in integrating a web UI for a more interactive testing experience, you can install the UI package with npm i -D @vitest/ui
and launch it by running npx vitest --ui
. This command will launch a web-based UI for Vitest. This UI provides an interactive way to view and manage your tests, making it easier to run tests, see their results in real-time, and monitor the overall status of your test suites.
For e2e testing, you might want to consider using Playwright or Cypress to simulate user interactions throughout your application. Playwright is an automation tool built on Node.js, designed for e2e testing and developed by Microsoft. It functions across major browser engines, including Chromium, WebKit, and Firefox, enabling automated tests across multiple browsers. Key features of Playwright include support for cross-browser, cross-platform, and cross-language testing, as well as native mobile web emulation for Google Chrome and Safari. The framework offers functionalities such as automatic waiting for web elements, network interception, and automatic retries for dynamic elements. Additionally, it supports execution tracing, screenshot and video capture, management of multiple tabs and origins, and the handling of iframes and shadow DOM.
Cypress is another automation tool used for testing web applications, primarily focusing on JavaScript for e2e testing. It is executed within the same context as the application when run in the browser, while running a Node.js server for actions outside the browser. This architecture allows Cypress to provide more consistent results by effectively understanding interactions both inside and outside the browser. Notable features of Cypress include real-time debugging with snapshots during test execution, network control, and capabilities for API and component testing, with tests written in either JavaScript or TypeScript. Furthermore, Cypress offers robust debugging tools that present readable errors and stack traces, automatic waiting for commands and assertions, direct execution of tests in the browser for enhanced speed, and support for custom commands that enable the creation of reusable and chainable actions.
When choosing a testing framework between Playwright and Cypress, Playwright offers a straightforward setup and configuration process. It also provides strong debugging capabilities with tools like Playwright Inspector, VSCode Debugger, and Browser Developer Tools. On the other hand, Cypress is known for its extensive documentation and a large community that effectively supports users in overcoming challenges. Playwright is ideal for experienced users who need complete coverage across various browsers, while Cypress is better suited for beginners seeking ease of installation and use.
To get started with Playwright, run npm init playwright@latest
to install it. This command will set up Playwright and create a test directory for storing your tests. To execute your tests, use npx playwright test
, which will run files in the test directory with the extensions .test.js, .test.ts, .spec.js, or .spec.ts. By default, tests are executed across all three browsers, Chromium, Firefox, and WebKit, using three workers. ou can customize this behavior in the playwright.config file. Tests run in headless mode, meaning no browser window will appear during execution. The results and logs will be displayed in the terminal. For more details on the syntax, refer to the Playwright API documentation.
To start using Cypress, run npm install -D cypress
to install the framework. This command sets up Cypress and creates a cypress directory for your test files. To run your tests, use npx cypress open
, which will launch the Cypress Test Runner, providing a graphical user interface (GUI) for easy test execution and debugging. When you execute this command, Cypress opens an application window that compiles your tests automatically and displays them for selection. This interactive environment allows you to run individual test files while offering real-time feedback on the test execution, enabling you to observe your application in action and inspect the DOM's state at each step. Additionally, Cypress includes several example tests located in the cypress/e2e directory. For further information on the syntax, you can refer to the Cypress API documentation.
foo@bar:~$ npm install -D vitest
foo@bar:~$ npx vitest run
foo@bar:~$ npm i -D @vitest/coverage-v8
foo@bar:~$ npx vitest run --coverage
foo@bar:~$ npm i -D @vitest/ui
foo@bar:~$ npx vitest run --ui
Unit testing
import { afterEach, beforeEach, describe, expect, test } from "vitest";
describe("Utilities", () => {
beforeEach(() => {});
afterEach(() => {});
test("someFunction()", () => {
expect(true).toBeTruthy();
});
});
Example of a unit test
foo@bar:~$ npm install -D vitest @testing-library/svelte jsdom
foo@bar:~$ npx vitest run
Component testing
export default defineConfig({
plugins: [svelte()],
...
test: {
environment: 'jsdom'
},
})
vitest.config.ts for component tests
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { render, screen, fireEvent } from "@testing-library/svelte";
import Component from "../Component.svelte";
describe("Component", () => {
test("that the Component is rendered", () => {
const props = {};
const { container } = render(Component, props);
render(Component);
expect(true).toBeTruthy();
});
});
Example of a component test
In the scenario where you want to mock Svelte stores with Vitest, the first step is to create a mock for your store to isolate the component you are testing. However, it is important to understand how Vitest handles mocking and hoisting, as this can lead to complications. When you use vi.mock, keep in mind that the mock is hoisted to the top of your file. This means that if you attempt to define local variables or re-mock within your tests, you may encounter issues. For example, if you declare a local store and then use it in your mock, problems may arise because the mock executes before that local variable is defined. To avoid this, you can use vi.hoisted to wrap your local variable definitions, ensuring they are prepared before the mock runs. If you prefer to keep your mocks organized in separate files, you can use dynamic imports. By using await import(), you can ensure that your imported mock is available before the mock definition, making it easier to manage more complex scenarios.
In the example below, the isLoggedIn and userDetails are mocked using two different approaches, vi.mock and vi.hoisted, along with a dynamic import. The vi.mock method is used to create a standard mock for one of the stores, while vi.hoisted is used to ensure that local variable definitions are available before the mock is executed. Additionally, dynamic imports enable the imported mock to be accessible before the mock definition.
import { writable } from "svelte/store";
export type User = { username: string; email: string };
export const isLoggedIn = writable(false);
export const userDetails = writable<User | null>(null);
user.ts
<script lang="ts">
import { isLoggedIn, userDetails } from "./user";
</script>
{#if $isLoggedIn}
{@const { username, email } = $userDetails ?? {}}
<p>Username: {username}</p>
<p>Email: {email}</p>
{:else}
<p>Login to your account</p>
{/if}
Profile.svelte
import { describe, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/svelte";
import { writable } from "svelte/store";
import { isLoggedIn, userDetails, type User } from "../user";
import Profile from "../Profile.svelte";
vi.mock("user", () => ({
isLoggedIn: writable(false),
userDetails: writable<User | null>(null),
}));
describe("Todo", () => {
test("that the Todo is rendered", () => {
isLoggedIn.set(true);
userDetails.set({
username: "user",
email: "user@mail.com",
});
const { container } = render(Profile);
expect(screen.queryByText("user")).toBeDefined();
});
test("that the Todo is rendered", () => {
isLoggedIn.set(true);
userDetails.set({
username: "user",
email: "user@mail.com",
});
const { container } = render(Profile);
expect(screen.queryByText("user")).toBeFalsy();
});
});
Profile.spec.ts using vi.mock
import { describe, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/svelte";
import Profile from "../Profile.svelte";
const { isLoggedIn, userDetails } = await vi.hoisted(() => import("../user"));
describe("Todo", () => {
test("that the Todo is rendered", () => {
isLoggedIn.set(true);
userDetails.set({
username: "user",
email: "user@mail.com",
});
const { container } = render(Profile);
expect(screen.queryByText("user")).toBeDefined();
});
test("that the Todo is rendered", () => {
isLoggedIn.set(false);
userDetails.set({
username: "user",
email: "user@mail.com",
});
const { container } = render(Profile);
expect(screen.queryByText("user")).toBeFalsy();
});
});
Profile.spec.ts using vi.hoisted and dynamic import
foo@bar:~$ npm init playwright@latest
foo@bar:~$ npx playwright test
E2E testing with Playwright
import { test, expect, type Page } from "@playwright/test";
test.beforeEach(async ({ page }) => {
await page.goto("https://demo.playwright.dev/todomvc");
});
const TODO_ITEMS = [
"buy some cheese",
"feed the cat",
"book a doctors appointment",
] as const;
test.describe("New Todo", () => {
test("should allow me to add todo items", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press("Enter");
// Make sure the list only has one todo item.
await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]);
// Create 2nd todo.
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press("Enter");
// Make sure the list now has two todo items.
await expect(page.getByTestId("todo-title")).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[1],
]);
await checkNumberOfTodosInLocalStorage(page, 2);
});
test("should clear text input field when an item is added", async ({
page,
}) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create one todo item.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press("Enter");
// Check that input is empty.
await expect(newTodo).toBeEmpty();
await checkNumberOfTodosInLocalStorage(page, 1);
});
test("should append new items to the bottom of the list", async ({
page,
}) => {
// Create 3 items.
await createDefaultTodos(page);
// create a todo count locator
const todoCount = page.getByTestId("todo-count");
// Check test using different methods.
await expect(page.getByText("3 items left")).toBeVisible();
await expect(todoCount).toHaveText("3 items left");
await expect(todoCount).toContainText("3");
await expect(todoCount).toHaveText(/3/);
// Check all items in one call.
await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS);
await checkNumberOfTodosInLocalStorage(page, 3);
});
});
test.describe("Mark all as completed", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test.afterEach(async ({ page }) => {
await checkNumberOfTodosInLocalStorage(page, 3);
});
test("should allow me to mark all items as completed", async ({ page }) => {
// Complete all todos.
await page.getByLabel("Mark all as complete").check();
// Ensure all todos have 'completed' class.
await expect(page.getByTestId("todo-item")).toHaveClass([
"completed",
"completed",
"completed",
]);
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
});
test("should allow me to clear the complete state of all items", async ({
page,
}) => {
const toggleAll = page.getByLabel("Mark all as complete");
// Check and then immediately uncheck.
await toggleAll.check();
await toggleAll.uncheck();
// Should be no completed classes.
await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]);
});
test("complete all checkbox should update state when items are completed / cleared", async ({
page,
}) => {
const toggleAll = page.getByLabel("Mark all as complete");
await toggleAll.check();
await expect(toggleAll).toBeChecked();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Uncheck first todo.
const firstTodo = page.getByTestId("todo-item").nth(0);
await firstTodo.getByRole("checkbox").uncheck();
// Reuse toggleAll locator and make sure its not checked.
await expect(toggleAll).not.toBeChecked();
await firstTodo.getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Assert the toggle all is checked again.
await expect(toggleAll).toBeChecked();
});
});
test.describe("Item", () => {
test("should allow me to mark items as complete", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
// Check first item.
const firstTodo = page.getByTestId("todo-item").nth(0);
await firstTodo.getByRole("checkbox").check();
await expect(firstTodo).toHaveClass("completed");
// Check second item.
const secondTodo = page.getByTestId("todo-item").nth(1);
await expect(secondTodo).not.toHaveClass("completed");
await secondTodo.getByRole("checkbox").check();
// Assert completed class.
await expect(firstTodo).toHaveClass("completed");
await expect(secondTodo).toHaveClass("completed");
});
test("should allow me to un-mark items as complete", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
const firstTodo = page.getByTestId("todo-item").nth(0);
const secondTodo = page.getByTestId("todo-item").nth(1);
const firstTodoCheckbox = firstTodo.getByRole("checkbox");
await firstTodoCheckbox.check();
await expect(firstTodo).toHaveClass("completed");
await expect(secondTodo).not.toHaveClass("completed");
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await firstTodoCheckbox.uncheck();
await expect(firstTodo).not.toHaveClass("completed");
await expect(secondTodo).not.toHaveClass("completed");
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
});
test("should allow me to edit an item", async ({ page }) => {
await createDefaultTodos(page);
const todoItems = page.getByTestId("todo-item");
const secondTodo = todoItems.nth(1);
await secondTodo.dblclick();
await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue(
TODO_ITEMS[1]
);
await secondTodo
.getByRole("textbox", { name: "Edit" })
.fill("buy some sausages");
await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter");
// Explicitly assert the new text value.
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
"buy some sausages",
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, "buy some sausages");
});
});
test.describe("Editing", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test("should hide other controls when editing", async ({ page }) => {
const todoItem = page.getByTestId("todo-item").nth(1);
await todoItem.dblclick();
await expect(todoItem.getByRole("checkbox")).not.toBeVisible();
await expect(
todoItem.locator("label", {
hasText: TODO_ITEMS[1],
})
).not.toBeVisible();
await checkNumberOfTodosInLocalStorage(page, 3);
});
test("should save edits on blur", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.fill("buy some sausages");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.dispatchEvent("blur");
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
"buy some sausages",
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, "buy some sausages");
});
test("should trim entered text", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.fill(" buy some sausages ");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.press("Enter");
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
"buy some sausages",
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, "buy some sausages");
});
test("should remove the item if an empty text string was entered", async ({
page,
}) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.press("Enter");
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test("should cancel edits on escape", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.fill("buy some sausages");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.press("Escape");
await expect(todoItems).toHaveText(TODO_ITEMS);
});
});
test.describe("Counter", () => {
test("should display the current number of todo items", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// create a todo count locator
const todoCount = page.getByTestId("todo-count");
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press("Enter");
await expect(todoCount).toContainText("1");
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press("Enter");
await expect(todoCount).toContainText("2");
await checkNumberOfTodosInLocalStorage(page, 2);
});
});
test.describe("Clear completed button", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
});
test("should display the correct text", async ({ page }) => {
await page.locator(".todo-list li .toggle").first().check();
await expect(
page.getByRole("button", { name: "Clear completed" })
).toBeVisible();
});
test("should remove completed items when clicked", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).getByRole("checkbox").check();
await page.getByRole("button", { name: "Clear completed" }).click();
await expect(todoItems).toHaveCount(2);
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test("should be hidden when there are no items that are completed", async ({
page,
}) => {
await page.locator(".todo-list li .toggle").first().check();
await page.getByRole("button", { name: "Clear completed" }).click();
await expect(
page.getByRole("button", { name: "Clear completed" })
).toBeHidden();
});
});
test.describe("Persistence", () => {
test("should persist its data", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
const todoItems = page.getByTestId("todo-item");
const firstTodoCheck = todoItems.nth(0).getByRole("checkbox");
await firstTodoCheck.check();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(["completed", ""]);
// Ensure there is 1 completed item.
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
// Now reload.
await page.reload();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(["completed", ""]);
});
});
test.describe("Routing", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
// make sure the app had a chance to save updated todos in storage
// before navigating to a new view, otherwise the items can get lost :(
// in some frameworks like Durandal
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
});
test("should allow me to display active items", async ({ page }) => {
const todoItem = page.getByTestId("todo-item");
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole("link", { name: "Active" }).click();
await expect(todoItem).toHaveCount(2);
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test("should respect the back button", async ({ page }) => {
const todoItem = page.getByTestId("todo-item");
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await test.step("Showing all items", async () => {
await page.getByRole("link", { name: "All" }).click();
await expect(todoItem).toHaveCount(3);
});
await test.step("Showing active items", async () => {
await page.getByRole("link", { name: "Active" }).click();
});
await test.step("Showing completed items", async () => {
await page.getByRole("link", { name: "Completed" }).click();
});
await expect(todoItem).toHaveCount(1);
await page.goBack();
await expect(todoItem).toHaveCount(2);
await page.goBack();
await expect(todoItem).toHaveCount(3);
});
test("should allow me to display completed items", async ({ page }) => {
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole("link", { name: "Completed" }).click();
await expect(page.getByTestId("todo-item")).toHaveCount(1);
});
test("should allow me to display all items", async ({ page }) => {
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole("link", { name: "Active" }).click();
await page.getByRole("link", { name: "Completed" }).click();
await page.getByRole("link", { name: "All" }).click();
await expect(page.getByTestId("todo-item")).toHaveCount(3);
});
test("should highlight the currently applied filter", async ({ page }) => {
await expect(page.getByRole("link", { name: "All" })).toHaveClass(
"selected"
);
//create locators for active and completed links
const activeLink = page.getByRole("link", { name: "Active" });
const completedLink = page.getByRole("link", { name: "Completed" });
await activeLink.click();
// Page change - active items.
await expect(activeLink).toHaveClass("selected");
await completedLink.click();
// Page change - completed items.
await expect(completedLink).toHaveClass("selected");
});
});
async function createDefaultTodos(page: Page) {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
}
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction((e) => {
return JSON.parse(localStorage["react-todos"]).length === e;
}, expected);
}
async function checkNumberOfCompletedTodosInLocalStorage(
page: Page,
expected: number
) {
return await page.waitForFunction((e) => {
return (
JSON.parse(localStorage["react-todos"]).filter(
(todo: any) => todo.completed
).length === e
);
}, expected);
}
async function checkTodosInLocalStorage(page: Page, title: string) {
return await page.waitForFunction((t) => {
return JSON.parse(localStorage["react-todos"])
.map((todo: any) => todo.title)
.includes(t);
}, title);
}
Example of a E2E test with Playwright
foo@bar:~$ npm install -D cypress
foo@bar:~$ npx cypress open
E2E testing with Cypress
/// <reference types="cypress" />
context("Actions", () => {
beforeEach(() => {
cy.visit("https://example.cypress.io/commands/actions");
});
// https://on.cypress.io/interacting-with-elements
it(".type() - type into a DOM element", () => {
// https://on.cypress.io/type
cy.get(".action-email").type("fake@email.com");
cy.get(".action-email").should("have.value", "fake@email.com");
// .type() with special character sequences
cy.get(".action-email").type("{leftarrow}{rightarrow}{uparrow}{downarrow}");
cy.get(".action-email").type("{del}{selectall}{backspace}");
// .type() with key modifiers
cy.get(".action-email").type("{alt}{option}"); //these are equivalent
cy.get(".action-email").type("{ctrl}{control}"); //these are equivalent
cy.get(".action-email").type("{meta}{command}{cmd}"); //these are equivalent
cy.get(".action-email").type("{shift}");
// Delay each keypress by 0.1 sec
cy.get(".action-email").type("slow.typing@email.com", { delay: 100 });
cy.get(".action-email").should("have.value", "slow.typing@email.com");
cy.get(".action-disabled")
// Ignore error checking prior to type
// like whether the input is visible or disabled
.type("disabled error checking", { force: true });
cy.get(".action-disabled").should("have.value", "disabled error checking");
});
it(".focus() - focus on a DOM element", () => {
// https://on.cypress.io/focus
cy.get(".action-focus").focus();
cy.get(".action-focus")
.should("have.class", "focus")
.prev()
.should("have.attr", "style", "color: orange;");
});
it(".blur() - blur off a DOM element", () => {
// https://on.cypress.io/blur
cy.get(".action-blur").type("About to blur");
cy.get(".action-blur").blur();
cy.get(".action-blur")
.should("have.class", "error")
.prev()
.should("have.attr", "style", "color: red;");
});
it(".clear() - clears an input or textarea element", () => {
// https://on.cypress.io/clear
cy.get(".action-clear").type("Clear this text");
cy.get(".action-clear").should("have.value", "Clear this text");
cy.get(".action-clear").clear();
cy.get(".action-clear").should("have.value", "");
});
it(".submit() - submit a form", () => {
// https://on.cypress.io/submit
cy.get(".action-form").find('[type="text"]').type("HALFOFF");
cy.get(".action-form").submit();
cy.get(".action-form")
.next()
.should("contain", "Your form has been submitted!");
});
it(".click() - click on a DOM element", () => {
// https://on.cypress.io/click
cy.get(".action-btn").click();
// You can click on 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// clicking in the center of the element is the default
cy.get("#action-canvas").click();
cy.get("#action-canvas").click("topLeft");
cy.get("#action-canvas").click("top");
cy.get("#action-canvas").click("topRight");
cy.get("#action-canvas").click("left");
cy.get("#action-canvas").click("right");
cy.get("#action-canvas").click("bottomLeft");
cy.get("#action-canvas").click("bottom");
cy.get("#action-canvas").click("bottomRight");
// .click() accepts an x and y coordinate
// that controls where the click occurs :)
cy.get("#action-canvas");
cy.get("#action-canvas").click(80, 75); // click 80px on x coord and 75px on y coord
cy.get("#action-canvas").click(170, 75);
cy.get("#action-canvas").click(80, 165);
cy.get("#action-canvas").click(100, 185);
cy.get("#action-canvas").click(125, 190);
cy.get("#action-canvas").click(150, 185);
cy.get("#action-canvas").click(170, 165);
// click multiple elements by passing multiple: true
cy.get(".action-labels>.label").click({ multiple: true });
// Ignore error checking prior to clicking
cy.get(".action-opacity>.btn").click({ force: true });
});
it(".dblclick() - double click on a DOM element", () => {
// https://on.cypress.io/dblclick
// Our app has a listener on 'dblclick' event in our 'scripts.js'
// that hides the div and shows an input on double click
cy.get(".action-div").dblclick();
cy.get(".action-div").should("not.be.visible");
cy.get(".action-input-hidden").should("be.visible");
});
it(".rightclick() - right click on a DOM element", () => {
// https://on.cypress.io/rightclick
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
// that hides the div and shows an input on right click
cy.get(".rightclick-action-div").rightclick();
cy.get(".rightclick-action-div").should("not.be.visible");
cy.get(".rightclick-action-input-hidden").should("be.visible");
});
it(".check() - check a checkbox or radio element", () => {
// https://on.cypress.io/check
// By default, .check() will check all
// matching checkbox or radio elements in succession, one after another
cy.get('.action-checkboxes [type="checkbox"]').not("[disabled]").check();
cy.get('.action-checkboxes [type="checkbox"]')
.not("[disabled]")
.should("be.checked");
cy.get('.action-radios [type="radio"]').not("[disabled]").check();
cy.get('.action-radios [type="radio"]')
.not("[disabled]")
.should("be.checked");
// .check() accepts a value argument
cy.get('.action-radios [type="radio"]').check("radio1");
cy.get('.action-radios [type="radio"]').should("be.checked");
// .check() accepts an array of values
cy.get('.action-multiple-checkboxes [type="checkbox"]').check([
"checkbox1",
"checkbox2",
]);
cy.get('.action-multiple-checkboxes [type="checkbox"]').should(
"be.checked"
);
// Ignore error checking prior to checking
cy.get(".action-checkboxes [disabled]").check({ force: true });
cy.get(".action-checkboxes [disabled]").should("be.checked");
cy.get('.action-radios [type="radio"]').check("radio3", { force: true });
cy.get('.action-radios [type="radio"]').should("be.checked");
});
it(".uncheck() - uncheck a checkbox element", () => {
// https://on.cypress.io/uncheck
// By default, .uncheck() will uncheck all matching
// checkbox elements in succession, one after another
cy.get('.action-check [type="checkbox"]').not("[disabled]").uncheck();
cy.get('.action-check [type="checkbox"]')
.not("[disabled]")
.should("not.be.checked");
// .uncheck() accepts a value argument
cy.get('.action-check [type="checkbox"]').check("checkbox1");
cy.get('.action-check [type="checkbox"]').uncheck("checkbox1");
cy.get('.action-check [type="checkbox"][value="checkbox1"]').should(
"not.be.checked"
);
// .uncheck() accepts an array of values
cy.get('.action-check [type="checkbox"]').check(["checkbox1", "checkbox3"]);
cy.get('.action-check [type="checkbox"]').uncheck([
"checkbox1",
"checkbox3",
]);
cy.get('.action-check [type="checkbox"][value="checkbox1"]').should(
"not.be.checked"
);
cy.get('.action-check [type="checkbox"][value="checkbox3"]').should(
"not.be.checked"
);
// Ignore error checking prior to unchecking
cy.get(".action-check [disabled]").uncheck({ force: true });
cy.get(".action-check [disabled]").should("not.be.checked");
});
it(".select() - select an option in a <select> element", () => {
// https://on.cypress.io/select
// at first, no option should be selected
cy.get(".action-select").should("have.value", "--Select a fruit--");
// Select option(s) with matching text content
cy.get(".action-select").select("apples");
// confirm the apples were selected
// note that each value starts with "fr-" in our HTML
cy.get(".action-select").should("have.value", "fr-apples");
cy.get(".action-select-multiple").select(["apples", "oranges", "bananas"]);
cy.get(".action-select-multiple")
// when getting multiple values, invoke "val" method first
.invoke("val")
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// Select option(s) with matching value
cy.get(".action-select").select("fr-bananas");
cy.get(".action-select")
// can attach an assertion right away to the element
.should("have.value", "fr-bananas");
cy.get(".action-select-multiple").select([
"fr-apples",
"fr-oranges",
"fr-bananas",
]);
cy.get(".action-select-multiple")
.invoke("val")
.should("deep.equal", ["fr-apples", "fr-oranges", "fr-bananas"]);
// assert the selected values include oranges
cy.get(".action-select-multiple")
.invoke("val")
.should("include", "fr-oranges");
});
it(".scrollIntoView() - scroll an element into view", () => {
// https://on.cypress.io/scrollintoview
// normally all of these buttons are hidden,
// because they're not within
// the viewable area of their parent
// (we need to scroll to see them)
cy.get("#scroll-horizontal button").should("not.be.visible");
// scroll the button into view, as if the user had scrolled
cy.get("#scroll-horizontal button").scrollIntoView();
cy.get("#scroll-horizontal button").should("be.visible");
cy.get("#scroll-vertical button").should("not.be.visible");
// Cypress handles the scroll direction needed
cy.get("#scroll-vertical button").scrollIntoView();
cy.get("#scroll-vertical button").should("be.visible");
cy.get("#scroll-both button").should("not.be.visible");
// Cypress knows to scroll to the right and down
cy.get("#scroll-both button").scrollIntoView();
cy.get("#scroll-both button").should("be.visible");
});
it(".trigger() - trigger an event on a DOM element", () => {
// https://on.cypress.io/trigger
// To interact with a range input (slider)
// we need to set its value & trigger the
// event to signal it changed
// Here, we invoke jQuery's val() method to set
// the value and trigger the 'change' event
cy.get(".trigger-input-range").invoke("val", 25);
cy.get(".trigger-input-range").trigger("change");
cy.get(".trigger-input-range")
.get("input[type=range]")
.siblings("p")
.should("have.text", "25");
});
it("cy.scrollTo() - scroll the window or element to a position", () => {
// https://on.cypress.io/scrollto
// You can scroll to 9 specific positions of an element:
// -----------------------------------
// | topLeft top topRight |
// | |
// | |
// | |
// | left center right |
// | |
// | |
// | |
// | bottomLeft bottom bottomRight |
// -----------------------------------
// if you chain .scrollTo() off of cy, we will
// scroll the entire window
cy.scrollTo("bottom");
cy.get("#scrollable-horizontal").scrollTo("right");
// or you can scroll to a specific coordinate:
// (x axis, y axis) in pixels
cy.get("#scrollable-vertical").scrollTo(250, 250);
// or you can scroll to a specific percentage
// of the (width, height) of the element
cy.get("#scrollable-both").scrollTo("75%", "25%");
// control the easing of the scroll (default is 'swing')
cy.get("#scrollable-vertical").scrollTo("center", { easing: "linear" });
// control the duration of the scroll (in ms)
cy.get("#scrollable-both").scrollTo("center", { duration: 2000 });
});
});
Example of a E2E test with Cypress
Accessibility
Accessibility (a11y) refers to the design and implementation of products, services, and environments that are accessible to people with disabilities, ensuring that everyone can use and benefit from them. This includes considerations for various types of disabilities, such as visual, auditory, physical, and cognitive impairments. a11y is a shorthand term for accessibility. The term is derived from the first and last letters of the word accessibility with the "11" representing the number of letters in between.
Svelte significantly improves accessibility by offering developers clear guidelines and warnings that help ensure compliance with established accessibility standards. For example, it emphasizes the importance of linking labels to form elements and provides warnings when labels are missing, which is crucial for helping screen reader users understand form fields. Additionally, it identifies issues related to semantic markup, such as empty headings or improper sequences of elements, and notifies developers if event handlers are assigned to non-interactive elements.
Keyboard navigation is a key component of accessibility, allowing users to navigate websites using only a keyboard. This is as important as offering alt text and ensuring high color contrast. Svelte includes features that support this, particularly by warning against the use of positive tabindex values. Using a positive tabindex can disrupt the expected focus order when navigating with the tab key, which can negatively impact user experience. Svelte also warns against using accesskey for creating custom keyboard shortcuts, as this could conflict with users' current shortcuts.
Furthermore, all non-text content, including images, audio, and video, should have alt text that provides the same information, which is a fundamental aspect of accessible design. Svelte issues warnings for images that do not include alt text, or for redundant tags. For example, it identifies that terms like "photo", "image" or "picture" in alt text are unnecessary since semantic markup already defines that it is an image. Additionally, there are warnings for tags that are hidden by default, which could prevent screen readers from announcing significant content.
Internationalization & Localization
Internationalization (i18n) and localization (l10n) are two related but separate processes in software development. i18n refers to internationalization, a term abbreviated by taking the first and last letters of the word, with the "18" signifying the number of letters in between. Similarly, l10n stands for localization, formed using the same method, where the "10" indicates the number of letters between the first and last letters.
i18n refers to the design and development of software in a way that allows it to be easily adapted for different languages and cultures, focusing on creating an adaptable structure that can integrate a range of linguistic and cultural variations. On the other hand, l10n is the actual process of adapting the software for a specific locale, which includes translating text and modifying elements such as date formats, currencies, and cultural references to meet the needs of a particular market.
Both i18n and l10n are crucial for businesses looking to expand their global presence, as they ensure that products provide a more relevant and personalized user experience. The main difference between the two is in their functions, i18n establishes the foundation for adaptability, while l10n implements the specific adjustments required for each individual market.
In Svelte, the third-party library svelte-i18n can be utilized to enable functionalities such as translation loading, message formatting, pluralization, and formatting for date, time, and currency, all of which enable internationalization in your application.
A typical localization process using the svelte-i18n library involves several key steps. First, you need to install the library by running npm install svelte-i18n
. Next, implement per-locale translation file loading using either the addMessages or register methods. The key difference is that addMessages operates synchronously, while register works asynchronously. This means that if you choose the addMessages method, you'll need to preload all translation files, whereas with register, only the files associated with the current locale, and the fallback locale, will be loaded.
Following this, initialize the library with an initial locale, which can be retrieved from the URL or the browser's navigator settings, along with a fallback locale. It is also important to consider locale direction differences, such as left-to-right versus right-to-left languages, and how they affect your application. Features like interpolation, which allows for the insertion of dynamic values into messages, enhance the flexibility and customization of text across different languages and contexts. This functionality, combined with support for number and date formatting, further enriches the user experience.
import {
addMessages,
getLocaleFromHash,
getLocaleFromQueryString,
init,
} from "svelte-i18n";
import en from "./translations/en.json";
import enUS from "./translations/en-US.json";
import elGR from "./translations/el-GR.json";
addMessages("en", en);
addMessages("en-US", enUS);
addMessages("el-GR", elGR);
await init({
fallbackLocale: "en",
initialLocale: getLocaleFromQueryString("lang") ?? getLocaleFromHash("lang"),
});
i18n.ts - Synchronous
import { getLocaleFromNavigator, init, register } from "svelte-i18n";
register("en", () => import("./translations/en.json"));
register("en-US", () => import("./translations/en-US.json"));
register("el-GR", () => import("./translations/el-GR.json"));
await init({
fallbackLocale: "en",
initialLocale: getLocaleFromNavigator(),
});
i18n.ts - Asynchronous
<script lang="ts">
import "./i18n";
import { date, locale, locales, number, time, _ } from "svelte-i18n";
</script>
<svelte:head>
<title>{$_("app.title")}</title>
</svelte:head>
<nav>
<a href="#login">{$_("app.sign-in")}</a>
<a href="#register">{$_("app.sign-up")}</a>
</nav>
<!-- Change locale -->
<select bind:value={$locale}>
{#each $locales as locale}
<option value={locale}>{locale}</option>
{/each}
</select>
<p>{$_("app.welcome", { values: { name: "user" } })}</p>
<div>{$date(Date.now())}</div>
<div>{$date(Date.now(), { format: "long" })}</div>
<div>{$time(Date.now())}</div>
<div>{$time(Date.now(), { format: "long" })}</div>
<div>{$number(12_345_678.9)}</div>
<div>{$number(0.91, { style: "percent" })}</div>
<div>{$number(6.99, { style: "currency", currency: "usd" })}</div>
<div>{$number(6.99, { style: "currency", currency: "eur" })}</div>
App.svelte
{
"app.title": "Application",
"app.home": "Home",
"app.welcome": "Welcome, {name}",
"app.sign-in": "Sign-in",
"app.sign-up": "Sign-up",
"pages.sign-in.username": "username",
"pages.sign-in.password": "password",
"pages.sign-up.fname": "First name",
"pages.sign-up.lname": "Last name",
"pages.sign-up.username": "Username",
"pages.sign-up.password": "Password"
}
translations/en.json
{
"app": {
"title": "Application",
"welcome": "Welcome, {name}",
"home": "Home",
"sign-in": "Login",
"sign-up": "Register"
},
"pages": {
"sign-in": {
"username": "username",
"password": "password"
},
"sign-up": {
"fname": "First name",
"lname": "Last name",
"username": "Username",
"password": "Password"
}
}
}
translations/en-US.json
{
"app": {
"title": "Εφαρμογή",
"welcome": "Καλωσήρθες, {name}",
"home": "Αρχική",
"sign-in": "Σύνδεση",
"sign-up": "Εγγραφή"
},
"pages": {
"sign-in": {
"username": "Όνομα Χρήστη",
"password": "Κωδικός Πρόσβασης"
},
"sign-up": {
"fname": "Όνομα",
"lname": "Επώνυμο",
"username": "Όνομα Χρήστη",
"password": "Κωδικός Πρόσβασης"
}
}
}
translations/el-GR.json
The code provided demonstrates how to implement localization in a Svelte application using the svelte-i18n library. It includes an i18n.ts file that uses two different approaches for handling translations. The first approach is synchronous, where translation files for English, US English, and Greek are loaded at the start using addMessages. The initial locale is determined from the URL query string or hash, defaulting to English if not specified. The second approach is asynchronous, using the register function to load translation files only when needed, which can enhance performance. Here, the initial locale is based on the browser's navigator settings, also defaulting to English if necessary. Both methods enable effective support for multiple languages within the application.
In the App component, the localization setup is imported from the i18n module along with functions for formatting dates, times, numbers, and retrieving translated strings. The page title is dynamically set in the <svelte:head>
using a localized string, and navigation links for Sign In and Sign Up are also localized. A dropdown allows users to change the application’s locale by binding the selected value to the $locale store, which displays all available locales. The code includes various examples of localized content, such as a welcome message that include a dynamic name, formatted dates and times, and different number formats, including standard, percentage, and currency styles for both USD and EUR.
The $format store serves as the formatter method, with aliases $_ *and *$t for convenience. In the example above, the $_ store is used. To format a message, simply execute the $format method. The locale store defines the current locale, and when its value changes, svelte-i18n checks for any registered message loaders for the new locale. If loaders are present, changing the locale becomes an asynchronous operation. Otherwise, it is synchronous. The <html lang>
attribute updates automatically to reflect the current locale. The $isLoading store can be used to determine if the app is fetching any queued message definitions, which is relevant when using the register function. The $dictionary store holds all loaded message definitions for each locale, and the structure of the dictionary can be either shallow or deep.
The translation files use two different structures for translation objects. The en.json file uses a shallow object structure, where each translation key is a flat string representing specific text in the application. While this format is straightforward, it can become complex with a growing number of keys, especially in larger applications. On the other hand, the en-US.json and el-GR.json files use a nested object format, organizing translations into hierarchical categories such as app and pages. This nested approach enhances clarity and maintainability, allowing developers to group related translations, which can simplify updates and modifications.
Note that everything is a store. That means that locale and dictionary can be set programmatically by calling the set method.
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 include 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.
Progressive Web App
A Progressive Web App (PWA) is a web application that merges the characteristics of traditional websites with the capabilities of native mobile applications. Using standard web technologies such as HTML, CSS, and JavaScript, PWAs are designed to be fast, reliable, and responsive, delivering a smooth experience across various devices and browsers. They have the ability to function offline, send push notifications, and can be installed directly on a user's device without needing to go through an app store. At its core, a PWA is a standard web application enhanced with specific components that provide app-like functionalities. These components consist of the manifest file, the service worker, and the requirement for HTTPS. The manifest file contains metadata, including the app's name, icons, and colors, which enables the app to be added to a device's home screen. Service workers enable app installation, offline functionality, caching, and background processes, while HTTPS enhances the overall security of the application. Once a PWA is installed, it can be updated automatically by checking for new versions of the service worker each time the app is opened. If an update is available, it is installed in the background and will activate only after all tabs using the previous version are closed, ensuring a smooth transition for the user. PWAs also manage cached assets to keep them current, providing a fresh and secure app-like experience across devices.
To implement a PWA in Svelte, start by creating a manifest.json file in the public directory. Link this manifest file in the <head>
section of your public/index.html. Then, create a service worker file, for example, service-worker.js, in the same directory. In your main Svelte component, likely App.svelte, register the service worker to enable its functionalities. After that, build your application using npm run build
and test it locally with a server to ensure it meets PWA criteria, such as offline capabilities and a valid manifest. Finally, deploy your PWA to a web server or static hosting service to make it accessible to users.
Effective cache management is crucial for maintaining updated assets in your PWA. You can achieve this by implementing an update strategy within your service worker. For instance, during the install event of the new service worker, you can delete old caches and load fresh assets by opening a new cache and adding the necessary files, such as HTML, CSS, and JavaScript. In the activate event, you can create a cache whitelist to remove any caches that are no longer needed, ensuring that only the latest assets are retained. This helps keep your app's resources current and enhances the user experience. A new service worker refers to an updated version of the service worker that has been modified compared to a previously installed version. When you make changes to the service worker file, the browser recognizes it as a new version. This new service worker goes through a lifecycle process that includes installation, activation, and control of the web application. When a new service worker is detected, it will be installed in the background, but it will not take control of the web application until all tabs using the old version are closed, unless you use commands like self.skipWaiting();
to activate it immediately.
{
"name": "Your App Name",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
./public/manifest.json
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("cache-v1").then((cache) => {
return cache.addAll([
"/",
"/index.html",
"/global.css",
"/build/bundle.css",
"/build/bundle.js",
"/icon-192x192.png",
"/icon-512x512.png",
]);
})
);
});
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
./public/service-worker.js
<script lang="ts">
...
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/service-worker.js")
.then((registration) => {
console.log("Service Worker registered with scope:", registration.scope);
})
.catch((error) => {
console.error("Service Worker registration failed:", error);
});
});
}
...
</script>
./src/App.svelte
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="manifest" href="/manifest.json">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
./index.html
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 preferred choices because of their customized services for hosting Svelte applications. These platforms offer efficient deployment procedures, scalability features, and additional tools to enable smooth deployment and maintenance. Essentially, you can use a Node.js application to serve the built Svelte application.
Imperative Component API
The main.js or main.ts file serves as the entry point for a Svelte application, enabling the configuration and initialization of Svelte components. Creating a component in Svelte involves defining a new instance of that component, which is essentially a JavaScript class that takes specific options. These options for initialization include target, a required property that specifies the HTMLElement or ShadowRoot where the component will be rendered, anchor, which indicates the position of the component within the target, props, an object containing the props to be passed to the component, and context, a Map that stores key-value pairs for root-level context, accessible via the Context API. This configuration results in a client-side component, meaning it is compiled by default with generate: 'dom'
, creating a JavaScript class.
Another property is the hydrate option, which can be set to true to upgrade existing DOM elements instead of creating new ones. However, using this option prevents the use of the anchor option. The hydrate option instructs Svelte to enhance existing DOM elements, typically from SSR, rather than generating new elements. This functionality is only applicable if the component was compiled with the hydratable: true
option. For proper hydration of <head>
elements, the SSR code must also be compiled with hydratable: true
, which adds a marker to each <head>
element, allowing the component to identify which elements it is responsible for removing during hydration. When hydrate: true is enabled, any existing children of the target will be removed, which is why the anchor option cannot be used simultaneously. Note that, the existing DOM does not need to match the component. Svelte will adjust and repair the DOM as needed during the hydration process.
Once the component is created, you can update props programmatically by using the component.$set(props)
method. To listen for events, the component.$on(event, callback)
method is used. Additionally, components can be removed from the DOM with the component.$destroy()
method.
For server-side components, HTML and CSS can be rendered through the Component.render() method, which returns an object containing the head, html, and css properties based on the specified props and options. This approach allows for effective management of component behavior and rendering in both client-side and server-side contexts.
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
answer: 'A' // `export let answer`
}
});
app.$set({ answer: 'B' });
// Callback invoked each time the component dispatches an "event" event
const clear = app.$on('event', console.log);
// A function is returned that will remove the event listener when called
clear();
// Removes component from the DOM and triggers any onDestroy handler
app.$destroy();
app.ts
// If the component is compiled with "accessors: true", which by default is set to false,
// each instance will have getters and setters corresponding to each of the component's props.
// Setting a value will cause a synchronous update, rather than the default asynchronous update caused by component.$set().
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
answer: 'A' // `export let answer`
}
});
console.log(app.answer);
app.answer = 'B';
app.ts with "accessors: true"
import App from './App.svelte';
const app = new App({
target: document.querySelector('#server-rendered-html'),
hydrate: true
});
app.ts with "hydrate" option
// To import a Svelte component directly into Node
require("svelte/register");
const App = require("./App.svelte").default;
const { head, html, css } = App.render(
{ answer: 42 },
{ context: new Map([["context-key", "context-value"]]) }
);
Svelte SSR
A ShadowRoot is a feature of the Web Components specification that enables developers to encapsulate a portion of a web page's DOM and CSS styles, creating a "shadow" DOM tree separate from the main document DOM. This encapsulation allows for the isolation of styles and scripts within a component, ensuring that styles defined in the ShadowRoot do not affect the rest of the document, and vice versa. Key benefits of using ShadowRoot include enhanced modularity and reusability of components, as developers can compose multiple ShadowRoots together. Additionally, it enables event handling within the Shadow DOM, allowing for interactions while maintaining encapsulation, and improves accessibility by providing a clear structure for assistive technologies.
Upgrading To Svelte 5
Svelte 5 removes implicit reactivity and introduces the Runes API, which implements reactivity through specific syntax. Runes are compiler instructions represented as functions that begin with a $. Support is also extended to .svelte.js and .svelte.ts files, which function like standard modules but enable the use of runes, making it easier to create reusable reactive logic and share reactive state across applications. As a result, stores become obsolete. While top-level let declarations in Svelte 4 are automatically reactive, Svelte 5 requires explicit reactivity using the $state rune for variable creation.
The $derived rune now handles the derivation of states that are managed by reactive statements in Svelte 4, while side effects controlled with $:
statements are managed through the $effect rune. Prop declarations are simplified in Svelte 5 with the $props rune, eliminating the need for separate declarations. Event listeners, which are attached using the on:
directive in Svelte 4, are treated as props in Svelte 5, allowing for shorthand syntax. The createEventDispatcher function for emitting events is deprecated, and components accept callback props to pass functions as properties. Additionally, DOM event modifiers are not compatible with event handlers in Svelte 5.
Svelte 5 also introduces snippets, which are reusable sections of markup within your components. This enhancement improves content management for components and results in the deprecation of slots. Additionally, while components in Svelte 4 are class-based, Svelte 5 shifts to a function-based approach, requiring different instantiation methods. This change affects how app instances are set up and how events are managed. The new function-based components do not include $on, $set, or $destroy methods.
Furthermore, with the release of Svelte 5, the Svelte CLI has been introduced to help developers easily scaffold their projects. This user-friendly interface allows you to create both Svelte and SvelteKit projects simply by running the CLI and following the prompts. It also offers integration with tools like Tailwind and Storybook right out of the box. In addition, the CLI includes built-in add-ons to simplify various tasks, such as formatting with Prettier, linting with ESLint, and testing with Vitest and Playwright. For database interactions, you can include Drizzle, while Lucia handles authentication, and Paraglide supports internationalization. Moreover, Svelte has created several specialized CLIs, including svelte-check for type-checking your projects and svelte-migrate to help you upgrade to new major versions. All these CLI tools are conveniently grouped under the sv command.
Conclusion
In conclusion, the examination of Svelte 4 showcases a robust framework that prioritizes simplicity and efficiency in the development of web UIs. Its innovative approach to reactivity and state management enables developers to build responsive applications with reduced complexity. By understanding its fundamental concepts, including data binding, component lifecycle, templating, event dispatching, and reactivity principles, developers can leverage Svelte's unique features to create engaging and interactive user experiences. The integration of modern tools like TailwindCSS and Flowbite, along with best practices in testing and accessibility, further enhances the development process.
As Svelte continues to grow, its community-focused approach and ecosystem make it an attractive option for both new and experienced developers. Adopting these practices will not only simplify project setup and organization but also guarantee that applications remain maintainable and scalable over time.
Top comments (0)