Svelte builds in flow control into its HTML Markup, here's a basic example.
{#if mobile}
<SomeMobileComponent />
{:else}
<SomeDesktopComponent />
{/if}
We could leave this post here and that would be the end of it. You can work out how to determine if a device is mobile, using whatever set of criteria you like. You build two components, one mobile and one desktop and your done, responsive right down to the HTML with little to no effort...
...but, this isn't particularly efficient for Svelte.js and especially not for SvelteKit. Why? Well there's (at least) three reasons:
SomeMobileComponent
and SomeDesktopComponent
share no components below this becomes incredibly bloatedThis approach though, is still largely the right way to do it, just with a few tweaks. Let's see what those tweaks are.
Firstly, unless your entire page changes depending on whether you're on mobile or desktop (and I promise in almost every case it doesn't) you don't need to do this switch at the root. Instead, determine what components are actually going to change between mobile and desktop, then do it at their root. For example, the menus on this site - AdamGreen.Tech - change depending on if we're on Mobile (default), Tablet (>600px) or Desktop (>1000px). Let's look at our components/Menu/index.svelte
.
<script>
import Desktop from './Desktop.svelte';
import Mobile from './Mobile.svelte';
import {device} from '$lib/utils/responsive.js';
</script>
{#if $device === 'desktop'}
<Desktop />
{:else}
<Mobile />
{/if}
Placing the switch at the root of the component you're actually changing removes the need to maintain two separate root components. This helps to fix the first problem, codebase bloat, by only maintaining separate Mobile, Desktop and Tablet components for the components that need to change between devices.
In most responsive situations, a lot of the layout isn't actually changing, but a few components are potentially being reordered. Take this blog post as an example. If you're viewing this post on desktop you'll notice that the metadata (author, publish date etc.) is shown on the right side of the display in a green box. On mobile this same box is moved inside the post and placed at the bottom. The responsive part isn't the box itself, just the position it's placed in.
So, in practice we have four files index.svelte
the root file that switches between desktop and mobile components Mobile.svelte
which is the mobile view Desktop.svelte
which is the desktop view, and Meta.svelte
which is the component that actually shows the meta data. Here's what they look like
<!-- index.svelte -->
<script>
import Desktop from './Desktop.svelte';
import Mobile from './Mobile.svelte';
import {device} from '$lib/utils/responsive.js';
</script>
{#if $device === 'desktop'}
<Desktop />
{:else}
<Mobile />
{/if}
<!-- Mobile.svelte -->
<script>
import Meta from './Meta.svelte';
</script>
<div class="border-pink">
<!-- Some blog stuff -->
<Meta />
</div>
<!-- Desktop.svelte -->
<script>
import Meta from './Meta.svelte';
</script>
<div class="border-pink left">
<!-- Some blog stuff -->
</div>
<div class="right">
<Meta />
</div>
Notice that Desktop.svelte
and Mobile.svelte
are nearly identical, except for the fact that the desktop version places the meta box in a separate <div>
which then is placed on the right hand side of the page.
By extracting out the Meta.svelte
component, we're essentially creating a shared component between the two responsive components. When Webpack, Rollup, Vite etc. comes to build this project, it'll create one function to represent the Meta.svelte
component which the functions representing both Desktop.svelte
and Mobile.svelte
will call in order to render the Meta component. If a particularly large component needs to be made responsive at a high level, regrouping shared components can ultimately help to reduce the size of the output.
One of the biggest issues this approach faces is utilising server-side rendering from SvelteKit effectively. Good web design says that a mobile view should scale well onto a desktop device without much change. However, depending on how the logic for deciding between mobile and desktop components is written, what tends to happen with this approach is the server builds a mobile view that hits a desktop, the logic takes over client-side and rerenders the view for desktop creating a dramatic cumulative layout shift.
At the end of the day, all these components are set up to default to their mobile component. What we should really be doing is determining server-side whether to return a mobile or desktop rendered page. If we figure this out, the cumulative layout shift will be negligible (and mostly a consequence of loading fonts, images etc.).
Here's what we're going to do:
hooks.js
file to pull a special header called Sec-CH-UA-Mobile
. This header will be set to "?1"
if a mobile view should be returned and "?0"
if a desktop view should be returned. We're going to stuff this information into the session object which all load functions have access to.routes/__layout.svelte
load function (which runs no matter what page is loaded) we'll add the boolean in the session information to the props of the layout component."desktop"
, "tablet"
or "mobile"
depending screen width and the server override.First let's look at the hooks.js
file located in src
.
// src/hooks.js
export function getSession(request) {
return {
mobile: request.headers['sec-ch-ua-mobile'] === '?1'
}
}
Essentially, every load
function for every svelte component will receive the returned object as session
. We can see this in the __layout.svelte
component below. The __layout.svelte
component is used as a wrapped for every page (assuming it's placed in src/routes
.
<!-- src/routes/__layout.svelte -->
<script context="module">
//the session object destructured below comes from the hooks.js file above
export async function load({ session }) {
return {
props: {
//Add the boolean as a property of the __layout.svelte component
overrideMobile: session.mobile,
}
}
}
</script>
<script>
import {width, mobile} from '$lib/utils/device';
//receive whether to override and render as mobile
export let overrideMobile = true;
//store this in the shared stores located in '$lib/utils/device'
$mobile = overrideMobile;
</script>
<!-- notice we also bind the inner width of the HTML document to a store -->
<svelte:window bind:innerWidth={$width} />
<slot></slot>
Notice that we also bind the innerWidth
of the HTML document (using svelte:window
) to another store in our $lib/utils/device.js
file.
// src/lib/utils/device.js
import {writable, derived} from 'svelte/store';
// a SvelteKit provided variable that indicates if we're on browser
import {browser} from '$app/env';
//default to 0 width - this won't be updated server side because svelte:window won't work server side
export const width = writable(0);
//this will be set server side, from the __layout.svelte component
export const mobile = writable(true);
//This store computes what device to render for
export const device = derived([width, mobile], ([width, mobile]) => {
// if width is >1000 (px) or we're not on browser and a mobile view hasn't been requested
if (width > 1000 || (!browser && !mobile)) {
//then we're on desktop
return 'desktop';
// if width is >420 (px) then we're on the browser and we're on a tablet
} else if (width > 420) {
// we're on a tablet
return 'tablet';
} else {
// if none of the previous conditions were satisfied, we're either not on a device >420px,
// or we're on the server, and a mobile view has been requested
return 'mobile';
}
});
Server-side, this logic essentially determines if we're on mobile, or desktop. Note that it can't determine server-side if the device is large enough to be considered a tablet, however, generally the approach we take is that a mobile view sufficiently scales to suit a tablet, and that the changes between mobile and tablet are not as stark as the changes between mobile and desktop (and therefore don't result in a large cumulative layout shift).
This approach allows you to do a rough and ready responsive view server-side. This is ideal particularly for SEO and bots that'll visit your site in both a tablet and desktop mode.
Svelte.js and SvelteKit let you do some pretty cool responsive work without bloating your code base, or the end output. More than that, you can deliver an SPA to client that is already SSR'd to their particular device (within reason) reducing the amount of time it takes to load and the layout shift they experience.