Best Practices
Best practices for working with Svelte and SvelteKit applications.
Svelte & SvelteKit
Svelte is our standard JavaScript framework for developing web-based applications.
What
Svelte was selected as our web framework of choice. SvelteKit is the meta-framework that helps build full stack web applications, using Svelte as its component framework.
How
Svelte and SvelteKit come with many features that serve a wide variety of web applications.
Svelte best practices
Some general rules to keep in mind:
Components SHOULD NOT contain page-level logic such as handling forms or manage global state. Try to keep component logic to a minimum and handle page state on the server.
A foundational or primitive component MAY be a global CSS class, or a component, but MUST NOT be both. Be sure to abstract highly reusable and mostly-atomic components.
Follow the project-configured formatter for code style. We RECOMMEND configuring your editor to autoformat files upon save.
The current tool for formatting
.svelte
files is Prettier and it comes with opinions such as formatting props with same-named variables to shorthand. For example:-<Button onclick={onclick}>Let's go</Button> +<Button {onclick}>Let's go</Button>
Prop names
Use
variant
to represent visual variants.Prefer single-word prop names, but use
camelCase
if necessary.Skeleton loaders SHOULD be separate components, and not a prop toggle.
Flags: props that toggle a feature via
true
orfalse
values are represented as plain adjectives without anis
prefix. Examples:expandable
,selectable
,selected
. Prefer to have them default tofalse
for easy truthy flagging. i.e. absence ofselectable
impliesselectable == false
, but the presence ofselected
toggles totrue
.HTML attributes are provided via Type declarations and should not be included as explicit props unless it has special treatment as part of a component.
- ✅ Appropriate prop inclusion:
disabled
for<Button>
. - ❌ Inappropriate prop inclusion:
aria-label
for<TextInput>
.
- ✅ Appropriate prop inclusion:
If a component contains an underlying interactive element, then a
ref
prop with 2-way binding should be provided to allow focusing via JS. If a component contains multiple focusable targets, those props MAY use a prefix such ascloseButtonRef
.Props with multiple options such as
size
should have all possible values explicitly typed, including defaults.✅ Good example
interface Props { size: 'sm' | 'default' | 'lg'; } type $$Props = Props; export const size: $$Props['size'] = 'default';
❌ Bad example
interface Props { size: 'sm' | 'lg'; } type $$Props = Props; export const size: $$Props['size'] = undefined;
Props that accept text SHOULD include a
Text
suffix. Example:helperText
. Such props MAY have a corresponding slot by the same name.
References:
Slots
All props that accept copy SHOULD have named slots with the same name as their respective prop provided, to allow formatted text or icons to be added.
If a prop and named slot is provided, the slot has priority and should render instead
Example:
<script> export let helperText; </script> <div> A gift for you. <slot name="helperText"> {#if helperText} <span>{helperText}</span> {/if} </slot> </div>
Events
Forward useful native events such as
on:click
.Prefer
pointer
events overmouse
events to be touch-friendly.Avoid
createEventDispatcher
since it will have obsolete utility in Svelte 5 thanks to event handlers, and providing callback functions is more ergonomic.Button.svelte
<script> export let ondoubleclick = () => {}; let lastClick = Date.now(); function checkDoubleClick() { if (Date.now() - lastClick < 500) { ondoubleclick(); } else { lastClick = Date.now(); } } </script> <button on:click on:click="{checkDoubleClick}">hi</button>
+page.svelte
<script> import Button from './Button.svelte'; function handleDoubleClick() { console.log('we did it'); } </script> <button ondoubleclick="{handleDoubleClick}">That's neat</button>
Respect a11y warnings
- a11y warnings SHOULD NOT be ignored as they often hint at a real issue.
- Among rare exceptions, these warnings break down when you’ve provided correct
attributes to an element that wraps a
<slot />
, whose contents fulfill the accessible experience. If this case applies, then those warnings MAY be ignored.
Handle user events, rather than depend on reactivity
Features such as reactive statements ($:
) are helpful to derive data to be
reused elsewhere. If that data comes from a user’s input, prefer using input
events instead as they are much easier to debug and maintain.
<script>
let value
</script>
-<input bind:value />
+<input on:change={e => value = e.target.value} />
If the derived or bound value relates to a form submission, use form actions instead. In React, this is known as “uncontrolled components” and it’s more suitable for progressive enhancement.
Prepare for Svelte 5
We write our applications in Svelte 4. When Svelte 5 launches, it will feature a refactoring tool, but it will not capture all cases. To prepare for its arrival, here are some recommendations:
Do not use CSS Nesting in
.svelte
files; Svelte 5 CSS will have new scoping behaviours for nested CSS.Avoid
createEventDispatcher
, use callback props instead such asexport let onchange
that accepts a function.<!-- Component.svelte --> <script> export let onchange; </script> <input type="checkbox" on:change="{onchange}" /> <!-- +page.svelte --> <script> import Component from './Component.svelte'; function onchange() {} </script> <Component {onchange} />
Which may be refactored in Svelte 5 as:
<!-- Component.svelte --> <script> export let onchange </script> -<input type="checkbox" on:change={onchange} /> +<input type="checkbox" {onchange} />
With this in mind, all your component consumers can remain the same, and only the component needs refactoring.
Do not bind to instance properties in an
{#each}
loop. Svelte 5 will treat instance properties as hard copies and Svelte 4’s “call by reference” behaviour will not work.<script> import SomeComponent from './SomeComponent.svelte' let myArr = [1, 2, 3] </script> - {#each myArr as item} + {#each myArr as item, i} - <SomeComponent bind:item /> + <SomeComponent bind:item={myArr[i]} /> {/each}
SvelteKit best practices
- Account for the no-JS user. On slow connections, it takes time for users to
see the page and have it be interactive upon hydration. Always provide
interactions that can work in plain HTML until the page hydrates, then provide
an enhanced experience. This means:
- Use form actions
- Consider regular hyperlinks, and GET endpoints for file downloads
- Use server-side redirects instead of
goto()
- Don’t abstract components unless it’s necessary. It’s okay to have large
+page.svelte
files, the “Don’t Repeat Yourself” methodology can work against you and lead to complicated views that span multiple files, making it troublesome to maintain. - Avoid shared state on the server, This includes imported stores or other globals. Doing this could leak information between users/requests. If you have request-specific data, use event.locals and load functions as documented by sveltekit.
File names
.ts
and.js
files MUST usekebab-case
, such asutils.js
orfield-options.js
. This includes test files..svelte
component files MUST usePascalCase
, such asDataTable.svelte
; exceptions are SvelteKit route files.
Variable names
- Functional cookies SHOULD be written in
snake_case
and have consistent categorical prefixes. Examples:auth_redirect_to
,auth_session_id
,form_data_new_campaign
. - Function names SHOULD use descriptive verbs and avoid abbreviations and other shorthand:
- ✅
handleClick()
,saveProfile()
- ❌
m()
,vnForceAccept()
- ✅
References:
User authentication and authorization
Users are authenticated in hooks.server.js
at every page request. From this
centralized location, we can redirect invalid sessions to the login page.
In a given route’s +page.server.js
, you may authorize the user for the given
page’s data if there are applicable permissions to account for not handled by
any upstream APIs. If a user isn’t authorized, or an API responds with a 403
response, then the page MAY return a 403
error.
Enhanced form actions
Part of our accessibility pledge is to provide operable and resilient experiences over a spectrum of user device capabilities. SSR and Form actions helps achieve this vision:
- SSR pages have a fast first contentful paint
<form>
elements can be submitted without JavaScript
Some key recommendations to keep in mind:
- Make use of
export let form
or$page.form
to server-render responses from a form submission, and use server-side redirects where applicable. - Modify the
use:enhance
function to display loading state while a form submission processes. This enhanced experience will be visible to users who are on a hydrated page.
Avoid client-side fetch or writing API endpoints
Client-side fetch requires JavaScript to function. In a progressively-enhanced app, it’s important to provide a minimal experience until the page hydrates.
Alternatives to client-side fetch include:
- Modify a page’s
load
to accept query parameters to fetch different data sets, which is useful for paginated content. - Use
<a>
links that target a page orGET
endpoint. - Use form actions.
It is acceptable to perform client-side fetch for situations that may require data polling or listening to server-sent events. Be sure to provide a manual ‘refresh’ button as part of the UI.
Conditional streaming with promises
In general, you may leverage SvelteKit’s streaming with
promises feature when
a given page’s API data consistently takes longer than 600ms
to respond. If
data is fast, then prefer server-rendered pages.
In SvelteKit server-side events such as load
, a
RequestEvent
object will provide the isDataRequest
property that can be used to identify
when enhanced navigation or enhanced form action requests are made by the user’s
browser. With this flag, you can determine whether to stream the response or
fully server-render the response.
For example:
// +page.server.js
import client from 'api-library';
export async function load({ isDataRequest }) {
const response = client.profiles.get();
return {
profiles: isDataRequest ? response : await response
};
}
With this feature, you may conditionally display skeleton loaders so that the user has immediate feedback on the state of the page:
<script>
export let data;
</script>
{#await data.profiles} Loading, showing skeleton components... {:then profiles}
<!-- show profiles -->
{/await}
More reading: https://geoffrich.net/posts/conditionally-stream-data/.
References
- Learn Svelte and SvelteKit: https://learn.svelte.dev/tutorial/introducing-sveltekit