- Introduction
- a. Basics
- b. Adding data
- c. Dynamic attributes
- d. Styling
- e. Nested components
- f. HTML tags
- g. Making an app
- Reactivity
- a. Assignments
- b. Declarations
- c. Statements
- d. Updating arrays and objects
- Props
- a. Declaring props
- b. Default values
- c. Spread props
- Logic
- a. If blocks
- b. Else blocks
- c. Else-if blocks
- d. Each blocks
- e. Keyed each blocks
- f. Await blocks
- Events
- a. DOM events
- b. Inline handlers
- c. Event modifiers
- d. Component events
- e. Event forwarding
- f. DOM event forwarding
- Bindings
- a. Text inputs
- b. Numeric inputs
- c. Checkbox inputs
- d. Group inputs : 인풋그룹 - 1 scoops 초코칩 아이스크림
- e. Textarea inputs
- f. Select bindings
- g. Select multiple
- h. Contenteditable bindings : textarea와 innerHTML연결
- i. Each block bindings
- j. Media elements : 비디오 재생 관련
- k. Dimensions : 사용자 정보 받아오기 관련
- l. This
- m. Component bindings
- n. Binding to component instances
- 7. Lifecycle
- a. onMount
- b. onDestroy
- c. beforeUpdate and afterUpdate
- d. tick
- 8. Stores
- a. Writable stores
- b. Auto-subscriptions
- c. Readable stores
- d. Derived stores
- e. Custom stores
- f. Store bindings
- 9. Motion
- a. Tweened
- b. Spring
- 10. Transitions
- a. The transition directive
- b. Adding parameters
- c. In and out
- d. Custom CSS transitions
- e. Custom JS transitions
- f. Transition events
- g. Local transitions
- h. Deferred transitions
- i. Key blocks
- 11. Animations
- a. The animate directive
- 12. Actions
- a. The use directive
- b. Adding parameters
- 13. Advanced styling
- a. The class directive
- b. Shorthand class directive
- c. Inline styles
- d. The style directive
- Component composition
- a. Slots
- b. Slot fallbacks
- c. Named slots
- d. Checking for slot content
- e. Slot props
- Context API
- a. setContext and getContext
- Special elements
- a. svelte:self
- b. svelte:component
- c. svelte:element
- d. svelte:window
- e. svelte:window bindings
- f. svelte:body
- g. svelte:head
- h. svelte:options
- i. svelte:fragment
- Module context
- a. Sharing code
- b. Exports
- Debugging
- a. The @debug tag 1
- Next steps
- a. Congratulations!
###props
So far, we've dealt exclusively with internal state — that is to say, the values are only accessible within a given component.
In any real application, you'll need to pass data from one component down to its children. To do that, we need to declare properties, generally shortened to 'props'. In Svelte, we do that with the export keyword. Edit the Nested.svelte component:
<script>
export let answer;
</script>
Just like $:, this may feel a little weird at first. That's not how export normally works in JavaScript modules! Just roll with it for now — it'll soon become second nature.
We can easily specify default values for props in Nested.svelte:
<script>
export let answer = 'a mystery';
</script>
If we now add a second component without an answer prop, it will fall back to the default:
<Nested answer={42}/>
<Nested/>
If you have an object of properties, you can 'spread' them onto a component instead of specifying each one:
<Info {...pkg}/>
Conversely, if you need to reference all the props that were passed into a component, including ones that weren't declared with export, you can do so by accessing $$props directly. It's not generally recommended, as it's difficult for Svelte to optimise, but it's useful in rare cases.
HTML doesn't have a way of expressing logic, like conditionals and loops. Svelte does.
To conditionally render some markup, we wrap it in an if block:
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{/if}
{#if !user.loggedIn}
<button on:click={toggle}>
Log in
</button>
{/if}
Try it — update the component, and click on the buttons.
Since the two conditions — if user.loggedIn and if !user.loggedIn — are mutually exclusive, we can simplify this component slightly by using an else block:
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}
A # character always indicates a block opening tag. A / character always indicates a block closing tag. A : character, as in {:else}, indicates a block continuation tag. Don't worry — you've already learned almost all the syntax Svelte adds to HTML.
Multiple conditions can be 'chained' together with else if:
{#if x > 10}
<p>{x} is greater than 10</p>
{:else if 5 > x}
<p>{x} is less than 5</p>
{:else}
<p>{x} is between 5 and 10</p>
{/if}
If you need to loop over lists of data, use an each block:
<ul>
{#each cats as cat}
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{cat.name}
</a></li>
{/each}
</ul>
The expression (cats, in this case) can be any array or array-like object (i.e. it has a length property). You can loop over generic iterables with each [...iterable].
You can get the current index as a second argument, like so:
{#each cats as cat, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{i + 1}: {cat.name}
</a></li>
{/each}
If you prefer, you can use destructuring — each cats as { id, name } — and replace cat.id and cat.name with id and name.
By default, when you modify the value of an each block, it will add and remove items at the end of the block, and update any values that have changed. That might not be what you want.
It's easier to show why than to explain. Click the 'Remove first thing' button a few times, and notice what happens: it does not remove the first <Thing> component, but rather the last DOM node. Then it updates the name value in the remaining DOM nodes, but not the emoji.
Instead, we'd like to remove only the first <Thing> component and its DOM node, and leave the others unaffected.
To do that, we specify a unique identifier (or "key") for the each block:
{#each things as thing (thing.id)}
<Thing name={thing.name}/>
{/each}
Here, (thing.id) is the key, which tells Svelte how to figure out which DOM node to change when the component updates.
You can use any object as the key, as Svelte uses a Map internally — in other words you could do (thing) instead of (thing.id). Using a string or number is generally safer, however, since it means identity persists without referential equality, for example when updating with fresh data from an API server.
Most web applications have to deal with asynchronous data at some point. Svelte makes it easy to await the value of promises directly in your markup:
{#await promise}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
Only the most recent promise is considered, meaning you don't need to worry about race conditions.
If you know that your promise can't reject, you can omit the catch block. You can also omit the first block if you don't want to show anything until the promise resolves:
{#await promise then value}
<p>the value is {value}</p>
{/await}
As we've briefly seen already, you can listen to any event on an element with the on: directive:
<div on:mousemove={handleMousemove}>
The mouse position is {m.x} x {m.y}
</div>
You can also declare event handlers inline:
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
The mouse position is {m.x} x {m.y}
</div>
The quote marks are optional, but they're helpful for syntax highlighting in some environments.
In some frameworks you may see recommendations to avoid inline event handlers for performance reasons, particularly inside loops. That advice doesn't apply to Svelte — the compiler will always do the right thing, whichever form you choose.
DOM event handlers can have modifiers that alter their behaviour. For example, a handler with a once modifier will only run a single time:
<script>
function handleClick() {
alert('no more alerts')
}
</script>
<button on:click|once={handleClick}>
Click me
</button>
The full list of modifiers:
- preventDefault — calls event.preventDefault() before running the handler. Useful for client-side form handling, for example.
- stopPropagation — calls event.stopPropagation(), preventing the event reaching the next element
- passive — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
- nonpassive — explicitly set passive: false
- capture — fires the handler during the capture phase instead of the bubbling phase (MDN docs)
- once — remove the handler after the first time it runs
- self — only trigger handler if event.target is the element itself
- trusted — only trigger handler if event.isTrusted is true. I.e. if the event is triggered by a user action.
You can chain modifiers together, e.g. on:click|once|capture={...}.
Components can also dispatch events. To do so, they must create an event dispatcher. Update Inner.svelte:
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
createEventDispatcher must be called when the component is first instantiated — you can't do it later inside e.g. a setTimeout callback. This links dispatch to the component instance.
Notice that the App component is listening to the messages dispatched by Inner component thanks to the on:message directive. This directive is an attribute prefixed with on: followed by the event name that we are dispatching (in this case, message).
Without this attribute, messages would still be dispatched, but the App would not react to it. You can try removing the on:message attribute and pressing the button again.
You can also try changing the event name to something else. For instance, change dispatch('message') to dispatch('myevent') in Inner.svelte and change the attribute name from on:message to on:myevent in the App.svelte component.
Unlike DOM events, component events don't bubble. If you want to listen to an event on some deeply nested component, the intermediate components must forward the event.
In this case, we have the same App.svelte and Inner.svelte as in the previous chapter, but there's now an Outer.svelte component that contains <Inner/>.
One way we could solve the problem is adding createEventDispatcher to Outer.svelte, listening for the message event, and creating a handler for it:
<script>
import Inner from './Inner.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function forward(event) {
dispatch('message', event.detail);
}
</script>
<Inner on:message={forward}/>
But that's a lot of code to write, so Svelte gives us an equivalent shorthand — an on:message event directive without a value means 'forward all message events'.
<script>
import Inner from './Inner.svelte';
</script>
<Inner on:message/>
Event forwarding works for DOM events too.
We want to get notified of clicks on our <CustomButton> — to do that, we just need to forward click events on the <button> element in CustomButton.svelte:
<button on:click>
Click me
</button>
As a general rule, data flow in Svelte is top down — a parent component can set props on a child component, and a component can set attributes on an element, but not the other way around.
Sometimes it's useful to break that rule. Take the case of the <input> element in this component — we could add an on:input event handler that sets the value of name to event.target.value, but it's a bit... boilerplatey. It gets even worse with other form elements, as we'll see.
Instead, we can use the bind:value directive:
<input bind:value={name}>
This means that not only will changes to the value of name update the input value, but changes to the input value will update name.
In the DOM, everything is a string. That's unhelpful when you're dealing with numeric inputs — type="number" and type="range" — as it means you have to remember to coerce input.value before using it.
With bind:value, Svelte takes care of it for you:
<input type=number bind:value={a} min=0 max=10>
<input type=range bind:value={a} min=0 max=10>
Checkboxes are used for toggling between states. Instead of binding to input.value, we bind to input.checked:
<input type=checkbox bind:checked={yes}>
If you have multiple inputs relating to the same value, you can use bind:group along with the value attribute. Radio inputs in the same group are mutually exclusive; checkbox inputs in the same group form an array of selected values.
Add bind:group to each input:
<input type=radio bind:group={scoops} name="scoops" value={1}>
In this case, we could make the code simpler by moving the checkbox inputs into an each block. First, add a menu variable to the <script> block...
let menu = [
'Cookies and cream',
'Mint choc chip',
'Raspberry ripple'
];
...then replace the second section:
<h2>Flavours</h2>
{#each menu as flavour}
<label>
<input type=checkbox bind:group={flavours} name="flavours" value={flavour}>
{flavour}
</label>
{/each}
It's now easy to expand our ice cream menu in new and exciting directions.
The <textarea> element behaves similarly to a text input in Svelte — use bind:value:
<textarea bind:value={value}></textarea>
In cases like these, where the names match, we can also use a shorthand form:
<textarea bind:value></textarea>
This applies to all bindings, not just textareas.
We can also use bind:value with <select> elements. Update line 20:
<select bind:value={selected} on:change="{() => answer = ''}">
Note that the <option> values are objects rather than strings. Svelte doesn't mind.
Because we haven't set an initial value of selected, the binding will set it to the default value (the first in the list) automatically. Be careful though — until the binding is initialised, selected remains undefined, so we can't blindly reference e.g. selected.id in the template. If your use case allows it, you could also set an initial value to bypass this problem.
A select can have a multiple attribute, in which case it will populate an array rather than selecting a single value.
Returning to our earlier ice cream example, we can replace the checkboxes with a <select multiple>:
<h2>Flavours</h2>
<select multiple bind:value={flavours}>
{#each menu as flavour}
<option value={flavour}>
{flavour}
</option>
{/each}
</select>
Press and hold the control key (or the command key on MacOS) for selecting multiple options.
Elements with a contenteditable="true" attribute support textContent and innerHTML bindings:
<div
contenteditable="true"
bind:innerHTML={html}
></div>
You can even bind to properties inside an each block.
{#each todos as todo}
<div class:done={todo.done}>
<input
type=checkbox
bind:checked={todo.done}
>
<input
placeholder="What needs to be done?"
bind:value={todo.text}
>
</div>
{/each}
Note that interacting with these <input> elements will mutate the array. If you prefer to work with immutable data, you should avoid these bindings and use event handlers instead.
The <audio> and <video> elements have several properties that you can bind to. This example demonstrates a few of them.
On line 62, add currentTime={time}, duration and paused bindings:
<video
poster="https://sveltejs.github.io/assets/caminandes-llamigos.jpg"
src="https://sveltejs.github.io/assets/caminandes-llamigos.mp4"
on:mousemove={handleMove}
on:touchmove|preventDefault={handleMove}
on:mousedown={handleMousedown}
on:mouseup={handleMouseup}
bind:currentTime={time}
bind:duration
bind:paused>
<track kind="captions">
</video>
bind:duration is equivalent to bind:duration={duration}
Now, when you click on the video, it will update time, duration and paused as appropriate. This means we can use them to build custom controls.
Ordinarily on the web, you would track currentTime by listening for timeupdate events. But these events fire too infrequently, resulting in choppy UI. Svelte does better — it checks currentTime using requestAnimationFrame.
The complete set of bindings for <audio> and <video> is as follows — six readonly bindings...
- duration (readonly) — the total duration of the video, in seconds
- buffered (readonly) — an array of {start, end} objects
- seekable (readonly) — ditto
- played (readonly) — ditto
- seeking (readonly) — boolean
- ended (readonly) — boolean
...and five two-way bindings:
- currentTime — the current point in the video, in seconds
- playbackRate — how fast to play the video, where 1 is 'normal'
- paused — this one should be self-explanatory
- volume — a value between 0 and 1
- muted — a boolean value where true is muted
Videos additionally have readonly videoWidth and videoHeight bindings.
Every block-level element has clientWidth, clientHeight, offsetWidth and offsetHeight bindings:
<div bind:clientWidth={w} bind:clientHeight={h}>
<span style="font-size: {size}px">{text}</span>
</div>
These bindings are readonly — changing the values of w and h won't have any effect.
Elements are measured using a technique similar to this one. There is some overhead involved, so it's not recommended to use this for large numbers of elements.
display: inline elements cannot be measured with this approach; nor can elements that can't contain other elements (such as <canvas>). In these cases you will need to measure a wrapper element instead.
The readonly this binding applies to every element (and component) and allows you to obtain a reference to rendered elements. For example, we can get a reference to a <canvas> element:
<canvas
bind:this={canvas}
width={32}
height={32}
></canvas>
Note that the value of canvas will be undefined until the component has mounted, so we put the logic inside the onMount lifecycle function.
Just as you can bind to properties of DOM elements, you can bind to component props. For example, we can bind to the value prop of this <Keypad> component as though it were a form element:
<Keypad bind:value={pin} on:submit={handleSubmit}/>
Now, when the user interacts with the keypad, the value of pin in the parent component is immediately updated.
Use component bindings sparingly. It can be difficult to track the flow of data around your application if you have too many of them, especially if there is no 'single source of truth'.
Just as you can bind to DOM elements, you can bind to component instances themselves. For example, we can bind the instance of <InputField> to a variable named field in the same way we did when binding DOM Elements
<script>
let field;
</script>
<InputField bind:this={field} />
Now we can programmatically interact with this component using field.
<button on:click="{() => field.focus()}">
Focus field
</button>
Note that we can't do {field.focus} since field is undefined when the button is first rendered and throws an error.
Lifecycle
Every component has a lifecycle that starts when it is created, and ends when it is destroyed. There are a handful of functions that allow you to run code at key moments during that lifecycle.
The one you'll use most frequently is onMount, which runs after the component is first rendered to the DOM. We briefly encountered it earlier when we needed to interact with a <canvas> element after it had been rendered.
We'll add an onMount handler that loads some data over the network:
<script>
import { onMount } from 'svelte';
let photos = [];
onMount(async () => {
const res = await fetch(`/tutorial/api/album`);
photos = await res.json();
});
</script>
It's recommended to put the fetch in onMount rather than at the top level of the <script> because of server-side rendering (SSR). With the exception of onDestroy, lifecycle functions don't run during SSR, which means we can avoid fetching data that should be loaded lazily once the component has been mounted in the DOM.
Lifecycle functions must be called while the component is initialising so that the callback is bound to the component instance — not (say) in a setTimeout.
If the onMount callback returns a function, that function will be called when the component is destroyed.
To run code when your component is destroyed, use onDestroy.
For example, we can add a setInterval function when our component initialises, and clean it up when it's no longer relevant. Doing so prevents memory leaks.
<script>
import { onDestroy } from 'svelte';
let counter = 0;
const interval = setInterval(() => counter += 1, 1000);
onDestroy(() => clearInterval(interval));
</script>
While it's important to call lifecycle functions during the component's initialisation, it doesn't matter where you call them from. So if we wanted, we could abstract the interval logic into a helper function in utils.js...
import { onDestroy } from 'svelte';
export function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);
onDestroy(() => {
clearInterval(interval);
});
}
...and import it into our component:
<script>
import { onInterval } from './utils.js';
let counter = 0;
onInterval(() => counter += 1, 1000);
</script>
Open and close the timer a few times and make sure the counter keeps ticking and the CPU load increases. This is due to a memory leak as the previous timers are not deleted. Don't forget to refresh the page before solving the example.
The beforeUpdate function schedules work to happen immediately before the DOM is updated. afterUpdate is its counterpart, used for running code once the DOM is in sync with your data.
Together, they're useful for doing things imperatively that are difficult to achieve in a purely state-driven way, like updating the scroll position of an element.
This Eliza chatbot is annoying to use, because you have to keep scrolling the chat window. Let's fix that.
let div;
let autoscroll;
beforeUpdate(() => {
autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
});
afterUpdate(() => {
if (autoscroll) div.scrollTo(0, div.scrollHeight);
});
Note that beforeUpdate will first run before the component has mounted, so we need to check for the existence of div before reading its properties.
The tick function is unlike other lifecycle functions in that you can call it any time, not just when the component first initialises. It returns a promise that resolves as soon as any pending state changes have been applied to the DOM (or immediately, if there are no pending state changes).
When you update component state in Svelte, it doesn't update the DOM immediately. Instead, it waits until the next microtask to see if there are any other changes that need to be applied, including in other components. Doing so avoids unnecessary work and allows the browser to batch things more effectively.
You can see that behaviour in this example. Select a range of text and hit the tab key. Because the <textarea> value changes, the current selection is cleared and the cursor jumps, annoyingly, to the end. We can fix this by importing tick...
import { tick } from 'svelte';
...and running it immediately before we set this.selectionStart and this.selectionEnd at the end of handleKeydown:
await tick();
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
Stores
Not all application state belongs inside your application's component hierarchy. Sometimes, you'll have values that need to be accessed by multiple unrelated components, or by a regular JavaScript module.
In Svelte, we do this with stores. A store is simply an object with a subscribe method that allows interested parties to be notified whenever the store value changes. In App.svelte, count is a store, and we're setting countValue in the count.subscribe callback.
Click the stores.js tab to see the definition of count. It's a writable store, which means it has set and update methods in addition to subscribe.
Now go to the Incrementer.svelte tab so that we can wire up the + button:
function increment() {
count.update(n => n + 1);
}
Clicking the + button should now update the count. Do the inverse for Decrementer.svelte.
Finally, in Resetter.svelte, implement reset:
function reset() {
count.set(0);
}
The app in the previous example works, but there's a subtle bug — the store is subscribed to, but never unsubscribed. If the component was instantiated and destroyed many times, this would result in a memory leak.
Start by declaring unsubscribe in App.svelte:
const unsubscribe = count.subscribe(value => {
countValue = value;
});
Calling a subscribe method returns an unsubscribe function.
You now declared unsubscribe, but it still needs to be called, for example through the onDestroy lifecycle hook:
<script>
import { onDestroy } from 'svelte';
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
let countValue;
const unsubscribe = count.subscribe(value => {
countValue = value;
});
onDestroy(unsubscribe);
</script>
<h1>The count is {countValue}</h1>
It starts to get a bit boilerplatey though, especially if your component subscribes to multiple stores. Instead, Svelte has a trick up its sleeve — you can reference a store value by prefixing the store name with $:
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
</script>
<h1>The count is {$count}</h1>
Auto-subscription only works with store variables that are declared (or imported) at the top-level scope of a component.
You're not limited to using $count inside the markup, either — you can use it anywhere in the <script> as well, such as in event handlers or reactive declarations.
Any name beginning with $ is assumed to refer to a store value. It's effectively a reserved character — Svelte will prevent you from declaring your own variables with a $ prefix.
Not all stores should be writable by whoever has a reference to them. For example, you might have a store representing the mouse position or the user's geolocation, and it doesn't make sense to be able to set those values from 'outside'. For those cases, we have readable stores.
Click over to the stores.js tab. The first argument to readable is an initial value, which can be null or undefined if you don't have one yet. The second argument is a start function that takes a set callback and returns a stop function. The start function is called when the store gets its first subscriber; stop is called when the last subscriber unsubscribes.
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return function stop() {
clearInterval(interval);
};
});
You can create a store whose value is based on the value of one or more other stores with derived. Building on our previous example, we can create a store that derives the time the page has been open:
export const elapsed = derived(
time,
$time => Math.round(($time - start) / 1000)
);
It's possible to derive a store from multiple inputs, and to explicitly set a value instead of returning it (which is useful for deriving values asynchronously). Consult the API reference for more information.
As long as an object correctly implements the subscribe method, it's a store. Beyond that, anything goes. It's very easy, therefore, to create custom stores with domain-specific logic.
For example, the count store from our earlier example could include increment, decrement and reset methods and avoid exposing set and update:
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
If a store is writable — i.e. it has a set method — you can bind to its value, just as you can bind to local component state.
In this example we have a writable store name and a derived store greeting. Update the <input> element:
<input bind:value={$name}>
Changing the input value will now update name and all its dependents.
We can also assign directly to store values inside a component. Add a <button> element:
<button on:click="{() => $name += '!'}">
Add exclamation mark!
</button>
The $name += '!' assignment is equivalent to name.set($name + '!').
Motion
Setting values and watching the DOM update automatically is cool. Know what's even cooler? Tweening those values. Svelte includes tools to help you build slick user interfaces that use animation to communicate changes.
Let's start by changing the progress store to a tweened value:
<script>
import { tweened } from 'svelte/motion';
const progress = tweened(0);
</script>
Clicking the buttons causes the progress bar to animate to its new value. It's a bit robotic and unsatisfying though. We need to add an easing function:
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const progress = tweened(0, {
duration: 400,
easing: cubicOut
});
</script>
The svelte/easing module contains the Penner easing equations, or you can supply your own p => t function where p and t are both values between 0 and 1.
The full set of options available to tweened:
- delay — milliseconds before the tween starts
- duration — either the duration of the tween in milliseconds, or a (from, to) => milliseconds function allowing you to (e.g.) specify longer tweens for larger changes in value
- easing — a p => t function
- interpolate — a custom (from, to) => t => value function for interpolating between arbitrary values. By default, Svelte will interpolate between numbers, dates, and identically-shaped arrays and objects (as long as they only contain numbers and dates or other valid arrays and objects). If you want to interpolate (for example) colour strings or transformation matrices, supply a custom interpolator
You can also pass these options to progress.set and progress.update as a second argument, in which case they will override the defaults. The set and update methods both return a promise that resolves when the tween completes.
The spring function is an alternative to tweened that often works better for values that are frequently changing.
In this example we have two stores — one representing the circle's coordinates, and one representing its size. Let's convert them to springs:
<script>
import { spring } from 'svelte/motion';
let coords = spring({ x: 50, y: 50 });
let size = spring(10);
</script>
Both springs have default stiffness and damping values, which control the spring's, well... springiness. We can specify our own initial values:
let coords = spring({ x: 50, y: 50 }, {
stiffness: 0.1,
damping: 0.25
});
Waggle your mouse around, and try dragging the sliders to get a feel for how they affect the spring's behaviour. Notice that you can adjust the values while the spring is still in motion.
Consult the API reference for more information.
Transitions
We can make more appealing user interfaces by gracefully transitioning elements into and out of the DOM. Svelte makes this very easy with the transition directive.
First, import the fade function from svelte/transition...
<script>
import { fade } from 'svelte/transition';
let visible = true;
</script>
...then add it to the <p> element:
<p transition:fade>Fades in and out</p>
Transition functions can accept parameters. Replace the fade transition with fly...
<script>
import { fly } from 'svelte/transition';
let visible = true;
</script>
...and apply it to the <p> along with some options:
<p transition:fly="{{ y: 200, duration: 2000 }}">
Flies in and out
</p>
Note that the transition is reversible — if you toggle the checkbox while the transition is ongoing, it transitions from the current point, rather than the beginning or the end.
Instead of the transition directive, an element can have an in or an out directive, or both together. Import fade alongside fly...
import { fade, fly } from 'svelte/transition';
...then replace the transition directive with separate in and out directives:
<p in:fly="{{ y: 200, duration: 2000 }}" out:fade>
Flies in, fades out
</p>
In this case, the transitions are not reversed.
The svelte/transition module has a handful of built-in transitions, but it's very easy to create your own. By way of example, this is the source of the fade transition:
function fade(node, {
delay = 0,
duration = 400
}) {
const o = +getComputedStyle(node).opacity;
return {
delay,
duration,
css: t => `opacity: ${t * o}`
};
}
The function takes two arguments — the node to which the transition is applied, and any parameters that were passed in — and returns a transition object which can have the following properties:
- delay — milliseconds before the transition begins
- duration — length of the transition in milliseconds
- easing — a p => t easing function (see the chapter on tweening)
- css — a (t, u) => css function, where u === 1 - t
- tick — a (t, u) => {...} function that has some effect on the node
The t value is 0 at the beginning of an intro or the end of an outro, and 1 at the end of an intro or beginning of an outro.
Most of the time you should return the css property and not the tick property, as CSS animations run off the main thread to prevent jank where possible. Svelte 'simulates' the transition and constructs a CSS animation, then lets it run.
For example, the fade transition generates a CSS animation somewhat like this:
0% { opacity: 0 }
10% { opacity: 0.1 }
20% { opacity: 0.2 }
/* ... */
100% { opacity: 1 }
We can get a lot more creative though. Let's make something truly gratuitous:
<script>
import { fade } from 'svelte/transition';
import { elasticOut } from 'svelte/easing';
let visible = true;
function spin(node, { duration }) {
return {
duration,
css: t => {
const eased = elasticOut(t);
return `
transform: scale(${eased}) rotate(${eased * 1080}deg);
color: hsl(
${Math.trunc(t * 360)},
${Math.min(100, 1000 - 1000 * t)}%,
${Math.min(50, 500 - 500 * t)}%
);`
}
};
}
</script>
Remember: with great power comes great responsibility.
While you should generally use CSS for transitions as much as possible, there are some effects that can't be achieved without JavaScript, such as a typewriter effect:
function typewriter(node, { speed = 1 }) {
const valid = (
node.childNodes.length === 1 &&
node.childNodes[0].nodeType === Node.TEXT_NODE
);
if (!valid) {
throw new Error(`This transition only works on elements with a single text node child`);
}
const text = node.textContent;
const duration = text.length / (speed * 0.01);
return {
duration,
tick: t => {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
It can be useful to know when transitions are beginning and ending. Svelte dispatches events that you can listen to like any other DOM event:
<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>
Ordinarily, transitions will play on elements when any container block is added or destroyed. In the example here, toggling the visibility of the entire list also applies transitions to individual list elements.
Instead, we'd like transitions to play only when individual items are added and removed — in other words, when the user drags the slider.
We can achieve this with a local transition, which only plays when the block with the transition itself is added or removed:
<div transition:slide|local>
{item}
</div>
A particularly powerful feature of Svelte's transition engine is the ability to defer transitions, so that they can be coordinated between multiple elements.
Take this pair of todo lists, in which toggling a todo sends it to the opposite list. In the real world, objects don't behave like that — instead of disappearing and reappearing in another place, they move through a series of intermediate positions. Using motion can go a long way towards helping users understand what's happening in your app.
We can achieve this effect using the crossfade function, which creates a pair of transitions called send and receive. When an element is 'sent', it looks for a corresponding element being 'received', and generates a transition that transforms the element to its counterpart's position and fades it out. When an element is 'received', the reverse happens. If there is no counterpart, the fallback transition is used.
Find the <label> element on line 65, and add the send and receive transitions:
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
Do the same for the next <label> element:
<label
class="done"
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
>
Now, when you toggle items, they move smoothly to their new location. The non-transitioning items still jump around awkwardly — we can fix that in the next chapter.
Key blocks destroy and recreate their contents when the value of an expression changes.
{#key value}
<div transition:fade>{value}</div>
{/key}
This is useful if you want an element to play its transition whenever a value changes instead of only when the element enters or leaves the DOM.
Wrap the <span> element in a key block depending on number. This will make the animation play whenever you press the increment button.
Animations
In the previous chapter, we used deferred transitions to create the illusion of motion as elements move from one todo list to the other.
To complete the illusion, we also need to apply motion to the elements that aren't transitioning. For this, we use the animate directive.
First, import the flip function — flip stands for 'First, Last, Invert, Play' — from svelte/animate:
import { flip } from 'svelte/animate';
Then add it to the <label> elements:
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip
>
The movement is a little slow in this case, so we can add a duration parameter:
<label
in:receive="{{key: todo.id}}"
out:send="{{key: todo.id}}"
animate:flip="{{duration: 200}}"
>
duration can also be a d => milliseconds function, where d is the number of pixels the element has to travel
Note that all the transitions and animations are being applied with CSS, rather than JavaScript, meaning they won't block (or be blocked by) the main thread.
Actions
Actions are essentially element-level lifecycle functions. They're useful for things like:
- interfacing with third-party libraries
- lazy-loaded images
- tooltips
- adding custom event handlers
In this app, we want to make the orange modal close when the user clicks outside it. It has an event handler for the outclick event, but it isn't a native DOM event. We have to dispatch it ourselves. First, import the clickOutside function...
import { clickOutside } from "./click_outside.js";
...then use it with the element:
<div class="box" use:clickOutside on:outclick="{() => (showModal = false)}">
Click outside me!
</div>
Open the click_outside.js file. Like transition functions, an action function receives a node (which is the element that the action is applied to) and some optional parameters, and returns an action object. That object can have a destroy function, which is called when the element is unmounted.
We want to fire the outclick event when the user clicks outside the orange box. One possible implementation looks like this:
export function clickOutside(node) {
const handleClick = (event) => {
if (!node.contains(event.target)) {
node.dispatchEvent(new CustomEvent("outclick"));
}
};
document.addEventListener("click", handleClick, true);
return {
destroy() {
document.removeEventListener("click", handleClick, true);
},
};
}
Update the clickOutside function, click the button to show the modal and then click outside it to close it.
Like transitions and animations, an action can take an argument, which the action function will be called with alongside the element it belongs to.
Here, we're using a longpress action that fires an event with the same name whenever the user presses and holds the button for a given duration. Right now, if you switch over to the longpress.js file, you'll see it's hardcoded to 500ms.
We can change the action function to accept a duration as a second argument, and pass that duration to the setTimeout call:
export function longpress(node, duration) {
// ...
const handleMousedown = () => {
timer = setTimeout(() => {
node.dispatchEvent(
new CustomEvent('longpress')
);
}, duration);
};
// ...
}
Back in App.svelte, we can pass the duration value to the action:
<button use:longpress={duration}
This almost works — the event now only fires after 2 seconds. But if you slide the duration down, it will still take two seconds.
To change that, we can add an update method in longpress.js. This will be called whenever the argument changes:
return {
update(newDuration) {
duration = newDuration;
},
// ...
};
If you need to pass multiple arguments to an action, combine them into a single object, as in use:longpress={{duration, spiciness}}
Advanced styling
Like any other attribute, you can specify classes with a JavaScript attribute, seen here:
<button
class="{current === 'foo' ? 'selected' : ''}"
on:click="{() => current = 'foo'}"
>foo</button>
This is such a common pattern in UI development that Svelte includes a special directive to simplify it:
<button
class:selected="{current === 'foo'}"
on:click="{() => current = 'foo'}"
>foo</button>
The selected class is added to the element whenever the value of the expression is truthy, and removed when it's falsy.
Often, the name of the class will be the same as the name of the value it depends on:
<div class:big={big}>
<!-- ... -->
</div>
In those cases we can use a shorthand form:
<div class:big>
<!-- ... -->
</div>
Apart from adding styles inside style tags, you can also add styles to individual elements using the style attribute. Usually you will want to do styling through CSS, but this can come in handy for dynamic styles, especially when combined with CSS custom properties.
Add the following style attribute to the paragraph element: style="color: {color}; --opacity: {bgOpacity};"
Great, now you can style the paragraph using variables that change based on your input without having to make a class for every possible value.
Being able to set CSS properties dynamically is nice. However, this can get unwieldy if you have to write a long string. Mistakes like missing any of the semicolons could make the whole string invalid. Therefore, Svelte provides a nicer way to write inline styles with the style directive.
Change the style attribute of the paragraph to the following:
<p
style:color
style:--opacity="{bgOpacity}"
>
The style directive shares a few qualities with the class directive. You can use a shorthand when the name of the property and the variable are the same. So style:color="{color}" can be written as just style:color.
Similar to the class directive, the style directive will take precedence if you try to set the same property through a style attribute.
Component composition
Just like elements can have children...
<div>
<p>I'm a child of the div</p>
</div>
...so can components. Before a component can accept children, though, it needs to know where to put them. We do this with the <slot> element. Put this inside Box.svelte:
<div class="box">
<slot></slot>
</div>
You can now put things in the box:
<Box>
<h2>Hello!</h2>
<p>This is a box. It can contain anything.</p>
</Box>
A component can specify fallbacks for any slots that are left empty, by putting content inside the <slot> element:
<div class="box">
<slot>
<em>no content was provided</em>
</slot>
</div>
We can now create instances of <Box> without any children:
<Box>
<h2>Hello!</h2>
<p>This is a box. It can contain anything.</p>
</Box>
<Box/>
The previous example contained a default slot, which renders the direct children of a component. Sometimes you will need more control over placement, such as with this <ContactCard>. In those cases, we can use named slots.
In ContactCard.svelte, add a name attribute to each slot:
<article class="contact-card">
<h2>
<slot name="name">
<span class="missing">Unknown name</span>
</slot>
</h2>
<div class="address">
<slot name="address">
<span class="missing">Unknown address</span>
</slot>
</div>
<div class="email">
<slot name="email">
<span class="missing">Unknown email</span>
</slot>
</div>
</article>
Then, add elements with corresponding slot="..." attributes inside the <ContactCard> component:
<ContactCard>
<span slot="name">
P. Sherman
</span>
<span slot="address">
42 Wallaby Way<br>
Sydney
</span>
</ContactCard>
In some cases, you may want to control parts of your component based on whether the parent passes in content for a certain slot. Perhaps you have a wrapper around that slot, and you don't want to render it if the slot is empty. Or perhaps you'd like to apply a class only if the slot is present. You can do this by checking the properties of the special $$slots variable.
$$slots is an object whose keys are the names of the slots passed in by the parent component. If the parent leaves a slot empty, then $$slots will not have an entry for that slot.
Notice that both instances of <Project> in this example render a container for comments and a notification dot, even though only one has comments. We want to use $$slots to make sure we only render these elements when the parent <App> passes in content for the comments slot.
In Project.svelte, update the class:has-discussion directive on the <article>:
<article class:has-discussion={$$slots.comments}>
Next, wrap the comments slot and its wrapping <div> in an if block that checks $$slots:
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
{/if}
Now the comments container and the notification dot won't render when <App> leaves the comments slot empty.
In this app, we have a <Hoverable> component that tracks whether the mouse is currently over it. It needs to pass that data back to the parent component, so that we can update the slotted contents.
For this, we use slot props. In Hoverable.svelte, pass the hovering value into the slot:
<div on:mouseenter={enter} on:mouseleave={leave}>
<slot hovering={hovering}></slot>
</div>
Remember you can also use the {hovering} shorthand, if you prefer.
Then, to expose hovering to the contents of the <Hoverable> component, we use the let directive:
<Hoverable let:hovering={hovering}>
<div class:active={hovering}>
{#if hovering}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
</div>
</Hoverable>
You can rename the variable, if you want — let's call it active in the parent component:
<Hoverable let:hovering={active}>
<div class:active>
{#if active}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
</div>
</Hoverable>
You can have as many of these components as you like, and the slotted props will remain local to the component where they're declared.
Named slots can also have props; use the let directive on an element with a slot="..." attribute, instead of on the component itself.
Context API
The context API provides a mechanism for components to 'talk' to each other without passing around data and functions as props, or dispatching lots of events. It's an advanced feature, but a useful one.
Take this example app using a Mapbox GL map. We'd like to display the markers, using the <MapMarker> component, but we don't want to have to pass around a reference to the underlying Mapbox instance as a prop on each component.
There are two halves to the context API — setContext and getContext. If a component calls setContext(key, context), then any child component can retrieve the context with const context = getContext(key).
Let's set the context first. In Map.svelte, import setContext from svelte and key from mapbox.js and call setContext:
import { onDestroy, setContext } from 'svelte';
import { mapbox, key } from './mapbox.js';
setContext(key, {
getMap: () => map
});
The context object can be anything you like. Like lifecycle functions, setContext and getContext must be called during component initialisation. Calling it afterwards - for example inside onMount - will throw an error. In this example, since map isn't created until the component has mounted, our context object contains a getMap function rather than map itself.
On the other side of the equation, in MapMarker.svelte, we can now get a reference to the Mapbox instance:
import { getContext } from 'svelte';
import { mapbox, key } from './mapbox.js';
const { getMap } = getContext(key);
const map = getMap();
The markers can now add themselves to the map.
A more finished version of <MapMarker> would also handle removal and prop changes, but we're only demonstrating context here.
Context keys
In mapbox.js you'll see this line:
const key = Symbol();
Technically, we can use any value as a key — we could do setContext('mapbox', ...) for example. The downside of using a string is that different component libraries might accidentally use the same one; using symbols, on the other hand, means that the keys are guaranteed not to conflict in any circumstance, even when you have multiple different contexts operating across many component layers, since a symbol is essentially a unique identifier.
Contexts vs. stores
Contexts and stores seem similar. They differ in that stores are available to any part of an app, while a context is only available to a component and its descendants. This can be helpful if you want to use several instances of a component without the state of one interfering with the state of the others.
In fact, you might use the two together. Since context is not reactive, values that change over time should be represented as stores:
const { these, are, stores } = getContext(...);
Speial elements
Svelte provides a variety of built-in elements. The first, <svelte:self>, allows a component to contain itself recursively.
It's useful for things like this folder tree view, where folders can contain other folders. In Folder.svelte we want to be able to do this...
{#if file.files}
<Folder {...file}/>
{:else}
<File {...file}/>
{/if}
...but that's impossible, because a module can't import itself. Instead, we use <svelte:self>:
{#if file.files}
<svelte:self {...file}/>
{:else}
<File {...file}/>
{/if}
A component can change its category altogether with <svelte:component>. Instead of a sequence of if blocks...
{#if selected.color === 'red'}
<RedThing/>
{:else if selected.color === 'green'}
<GreenThing/>
{:else if selected.color === 'blue'}
<BlueThing/>
{/if}
...we can have a single dynamic component:
<svelte:component this={selected.component}/>
The this value can be any component constructor, or a falsy value — if it's falsy, no component is rendered.
Sometimes we don't know in advance what kind of DOM element to render. <svelte:element> comes in handy here. Instead of a sequence of if blocks...
{#if selected === 'h1'}
<h1>I'm a h1 tag</h1>
{:else if selected === 'h3'}
<h3>I'm a h3 tag</h3>
{:else if selected === 'p'}
<p>I'm a p tag</p>
{/if}
...we can have a single dynamic component:
<svelte:element this={selected}>I'm a {selected} tag</svelte:element>
The this value can be any string, or a falsy value — if it's falsy, no element is rendered.
Just as you can add event listeners to any DOM element, you can add event listeners to the window object with <svelte:window>.
On line 11, add the keydown listener:
<svelte:window on:keydown={handleKeydown}/>
As with DOM elements, you can add event modifiers like preventDefault.
We can also bind to certain properties of window, such as scrollY. Update line 7:
<svelte:window bind:scrollY={y}/>
The list of properties you can bind to is as follows:
- innerWidth
- innerHeight
- outerWidth
- outerHeight
- scrollX
- scrollY
- online — an alias for window.navigator.onLine
All except scrollX and scrollY are readonly.
Similar to <svelte:window>, the <svelte:body> element allows you to listen for events that fire on document.body. This is useful with the mouseenter and mouseleave events, which don't fire on window.
Add the mouseenter and mouseleave handlers to the <svelte:body> tag:
<svelte:body
on:mouseenter={handleMouseenter}
on:mouseleave={handleMouseleave}
/>
The <svelte:head> element allows you to insert elements inside the <head> of your document:
<svelte:head>
<link rel="stylesheet" href="/tutorial/dark-theme.css">
</svelte:head>
In server-side rendering (SSR) mode, contents of <svelte:head> are returned separately from the rest of your HTML.
The <svelte:options> element allows you to specify compiler options.
We'll use the immutable option as an example. In this app, the <Todo> component flashes whenever it receives new data. Clicking on one of the items toggles its done state by creating an updated todos array. This causes the other <Todo> items to flash, even though they don't end up making any changes to the DOM.
We can optimise this by telling the <Todo> component to expect immutable data. This means that we're promising never to mutate the todo prop, but will instead create new todo objects whenever things change.
Add this to the top of the Todo.svelte file:
<svelte:options immutable={true}/>
You can shorten this to <svelte:options immutable/> if you prefer.
Now, when you toggle todos by clicking on them, only the updated component flashes.
The options that can be set here are:
- immutable={true} — you never use mutable data, so the compiler can do simple referential equality checks to determine if values have changed
- immutable={false} — the default. Svelte will be more conservative about whether or not mutable objects have changed
- accessors={true} — adds getters and setters for the component's props
- accessors={false} — the default
- namespace="..." — the namespace where this component will be used, most commonly "svg"
- tag="..." — the name to use when compiling this component as a custom element
Consult the API reference for more information on these options.
The <svelte:fragment> element allows you to place content in a named slot without wrapping it in a container DOM element. This keeps the flow layout of your document intact.
In the example notice how we applied a flex layout with a gap of 1em to the box.
<!-- Box.svelte -->
<div class="box">
<slot name="header">No header was provided</slot>
<p>Some content between header and footer</p>
<slot name="footer"></slot>
</div>
<style>
.box {
display: flex;
flex-direction: column;
gap: 1em;
}
</style>
However, the content in the footer is not spaced out according to this rhythm because wrapping it in a div created a new flow layout.
We can solve this by changing <div slot="footer"> in the App component. Replace the <div> with <svelte:fragment>:
<svelte:fragment slot="footer">
<p>All rights reserved.</p>
<p>Copyright (c) 2019 Svelte Industries</p>
</svelte:fragment>
Module Context
In all the examples we've seen so far, the <script> block contains code that runs when each component instance is initialised. For the vast majority of components, that's all you'll ever need.
Very occasionally, you'll need to run some code outside of an individual component instance. For example, you can play all five of these audio players simultaneously; it would be better if playing one stopped all the others.
We can do that by declaring a <script context="module"> block. Code contained inside it will run once, when the module first evaluates, rather than when a component is instantiated. Place this at the top of AudioPlayer.svelte:
<script context="module">
let current;
</script>
It's now possible for the components to 'talk' to each other without any state management:
function stopOthers() {
if (current && current !== audio) current.pause();
current = audio;
}
Anything exported from a context="module" script block becomes an export from the module itself. If we export a stopAll function from AudioPlayer.svelte...
<script context="module">
const elements = new Set();
export function stopAll() {
elements.forEach(element => {
element.pause();
});
}
</script>
...we can then import it in App.svelte...
<script>
import AudioPlayer, { stopAll } from './AudioPlayer.svelte';
</script>
...and use it in an event handler:
<button on:click={stopAll}>
stop all audio
</button>
You can't have a default export, because the component is the default export.
Debugging
Occasionally, it's useful to inspect a piece of data as it flows through your app.
One approach is to use console.log(...) inside your markup. If you want to pause execution, though, you can use the {@debug ...} tag with a comma-separated list of values you want to inspect:
{@debug user}
<h1>Hello {user.firstname}!</h1>
If you now open your devtools and start interacting with the <input> elements, you'll trigger the debugger as the value of user changes.
'자바스크립트 > Svelte' 카테고리의 다른 글
[간단] 스벨트에서 localStrorage 안될 때 (0) | 2022.12.04 |
---|---|
[번역] setContext and getContext (0) | 2022.11.23 |
[번역] svelteKit -fetch requests (0) | 2022.11.11 |
[svelteKit] 상위 레이아웃 무시하기 (0) | 2022.11.11 |
[한글] SvelteKit - Project Structure (0) | 2022.10.27 |
댓글