Skip to content

Instantly share code, notes, and snippets.

@WarningImHack3r
Last active February 8, 2025 21:54
Show Gist options
  • Save WarningImHack3r/375c559c5ee120408f9df2390ec2747a to your computer and use it in GitHub Desktop.
Save WarningImHack3r/375c559c5ee120408f9df2390ec2747a to your computer and use it in GitHub Desktop.
ScreenSize Tailwind component for Svelte

Inspired by this gist (Tweet)

Added original Gist's suggested comment and a border + shadow for higher contrast.

Note

The Svelte 5 version is now available in the ScreenSizeV2_Svelte5.svelte file!

To use it, bring it into your +layout.svelte as follows:

<script>
	import { dev } from "$app/environment";
	import ScreenSize from "$lib/components/ScreenSize.svelte";
</script>


{#if dev}
	<ScreenSize />
{/if}

...

v2

ScreenSizeV2 is a personal improvement to the original component. It adds:

  • Dynamic breakpoint fetching from the Tailwind config
  • Toggleable sliding menu listing all your existing breakpoints

Tailwind 4

The Tailwind-4-compatible v2 version is now self-contained, completely standalone, and Svelte-5-native. It's unfortunately quite chonkier than the Tailwind 3 edition, due to the CSS to JS conversion we have to manually handle now.
It however supports other units than px, uses bleeding-edge Svelte 5 additions, and now highlights the current screen size in its menu!

It's kind of a v3, but more of a v2.5: a v2 version with some tiny tweaks to reward you for your Tailwind 4 migration!

Important

Since Tailwind 4.0.5, you'll need to change your imports at the top of your CSS file to avoid tree-shaking breakpoint variables.

-@import "tailwindcss";
+@layer theme, base, components, utilites;
+
+@import "tailwindcss/theme" layer(theme) theme(static);
+@import "tailwindcss/preflight" layer(base);
+@import "tailwindcss/utilities" layer(utilities);

You'll also have to add the static option to your @theme:

-@theme {
+@theme static {

This has however the drawbacks of including all, even unused, CSS variables inside your bundle.
Reference discussion and docs link.

Tailwind 3 (old)

To use the v2 for Tailwind 3, you'll need to tweak your Vite config and your Tailwind config too in order to import the Tailwind config into your code. Here's what you have to do:

Vite

Add this option to allow Vite to fetch files anywhere in your project, not only in your source code. This allows importing the Tailwind config inside your code.

// vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";

export default defineConfig({
-	plugins: [sveltekit()]
+	plugins: [sveltekit()],
+	server: {
+		fs: {
+			strict: false // `allow: ["."]` also works
+		}
+	}
});

Tailwind

Make sure to use the correct import for tailwindcss/defaultTheme if you want to use custom breakpoints:

import type { Config } from "tailwindcss";
- import { screens } from "tailwindcss/defaultTheme";
+ import defaultTheme from "tailwindcss/defaultTheme";
import typography from "@tailwindcss/typography";

export default {
	content: ["./src/**/*.{html,js,svelte,ts}"],
	theme: {
		screens: {
			xs: "475px",
-			screens
+			...defaultTheme.screens
		},
...
<script>
// Made by https://github.com/WarningImHack3r
// Inspired by https://gist.github.com/Sh4yy/0300299ae60af4910bcb341703946330
let width = 0;
let height = 0;
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<div
class="fixed bottom-5 left-5 z-50 flex items-center space-x-2 rounded-full border shadow-lg shadow-black/50 border-gray-500 bg-black px-2.5 py-1 font-mono text-xs font-medium text-white"
>
<span>
{width.toLocaleString()} x {height.toLocaleString()}
</span>
<div class="h-4 w-px bg-gray-800" />
<span class="sm:hidden">XS</span>
<span class="hidden sm:max-md:inline">SM</span>
<span class="hidden md:max-lg:inline">MD</span>
<span class="hidden lg:max-xl:inline">LG</span>
<span class="hidden xl:max-2xl:inline">XL</span>
<span class="max-2xl:hidden">2XL</span>
</div>
<script lang="ts">
// From https://gist.github.com/WarningImHack3r/375c559c5ee120408f9df2390ec2747a
// Inspired by https://gist.github.com/Sh4yy/0300299ae60af4910bcb341703946330
import { slide } from "svelte/transition";
import tailwindConfig from "../../tailwind.config";
import resolveConfig from "tailwindcss/resolveConfig";
const fullConfig = resolveConfig(tailwindConfig);
const configScreens = fullConfig.theme.screens;
const screens = Object.keys(configScreens)
.map(screen => ({
name: screen,
size: parseInt(configScreens[screen as keyof typeof configScreens].replace("px", ""))
}))
.sort((a, b) => a.size - b.size);
let matchingScreen: (typeof screens)[number] | undefined;
$: matchingScreen = screens.findLast(screen => screen.size <= width);
let width = 0;
let height = 0;
let showAllScreens = false;
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<div
class="font-mono text-xs font-medium text-white *:fixed *:border *:border-gray-500 *:bg-black *:px-2.5 *:py-1 *:shadow-lg *:shadow-black/50"
>
<div class="bottom-5 left-5 z-50 flex items-center rounded-full !pr-1">
<span>
{width.toLocaleString()} x {height.toLocaleString()}
</span>
{#if matchingScreen}
<div class="ml-1.5 mr-1 h-4 w-px bg-gray-800"></div>
<button
type="button"
class="inline rounded-l-sm rounded-r-full px-1 hover:bg-neutral-500 active:bg-neutral-600"
on:click={() => (showAllScreens = !showAllScreens)}
>
{matchingScreen.name.toUpperCase()}
</button>
{:else}
<div class="mr-1"></div>
{/if}
</div>
{#if showAllScreens}
<div class="bottom-12 left-5 z-40 rounded-xl duration-300" transition:slide>
{#each screens as screen}
<div class="flex justify-between gap-8">
<span>{screen.name.toUpperCase()}</span>
<span class="text-neutral-400">{screen.size.toLocaleString()}px</span>
</div>
{/each}
</div>
{/if}
</div>
<script lang="ts">
// From https://gist.github.com/WarningImHack3r/375c559c5ee120408f9df2390ec2747a
// Inspired by https://gist.github.com/Sh4yy/0300299ae60af4910bcb341703946330
import { slide } from "svelte/transition";
import tailwindConfig from "../../../tailwind.config";
import resolveConfig from "tailwindcss/resolveConfig";
const fullConfig = resolveConfig(tailwindConfig);
const configScreens = fullConfig.theme.screens;
const screens = Object.keys(configScreens)
.map(screen => ({
name: screen,
size: parseInt(configScreens[screen as keyof typeof configScreens].replace("px", ""))
}))
.sort((a, b) => a.size - b.size);
let width = $state(0);
let height = $state(0);
let matchingScreen = $derived(screens.findLast(screen => screen.size <= width));
let showAllScreens = $state(false);
</script>
<svelte:window bind:innerWidth={width} bind:innerHeight={height} />
<div
class="font-mono text-xs font-medium text-white *:fixed *:border *:border-gray-500 *:bg-black *:px-2.5 *:py-1 *:shadow-lg *:shadow-black/50"
>
<div class="bottom-5 left-5 z-50 flex items-center rounded-full !pr-1">
<span>
{width.toLocaleString()} x {height.toLocaleString()}
</span>
{#if matchingScreen}
<div class="ml-1.5 mr-1 h-4 w-px bg-gray-800"></div>
<button
type="button"
class="inline rounded-l-sm rounded-r-full px-1 hover:bg-neutral-500 active:bg-neutral-600"
onclick={() => (showAllScreens = !showAllScreens)}
>
{matchingScreen.name.toUpperCase()}
</button>
{:else}
<div class="mr-1"></div>
{/if}
</div>
{#if showAllScreens}
<div class="bottom-12 left-5 z-40 rounded-xl duration-300" transition:slide>
{#each screens as screen}
<div class="flex justify-between gap-8">
<span>{screen.name.toUpperCase()}</span>
<span class="text-neutral-400">{screen.size.toLocaleString()}px</span>
</div>
{/each}
</div>
{/if}
</div>
<script lang="ts">
// From https://gist.github.com/WarningImHack3r/375c559c5ee120408f9df2390ec2747a
// Inspired by https://gist.github.com/Sh4yy/0300299ae60af4910bcb341703946330
import { innerWidth, innerHeight } from "svelte/reactivity/window";
import { slide } from "svelte/transition";
let screens = $state<{ name: string; size: number }[]>([]);
let width = $derived(innerWidth.current ?? 0);
let height = $derived(innerHeight.current ?? 0);
function convertToPixels(rootStyles: CSSStyleDeclaration, value: string | number) {
if (typeof value === "number") return value;
const num = parseFloat(value);
const unit = value.match(/[a-z]+$/i)?.[0]?.toLowerCase();
if (!num || !unit) return null;
// unit-to-px multiplicators
const conversions = {
px: 1,
em: 16,
rem: parseFloat(rootStyles.fontSize),
vw: width / 100,
vh: height / 100
// add other units here if you want
};
return num * (conversions[unit as keyof typeof conversions] || 0);
}
let matchingScreen = $derived(screens.findLast(screen => screen.size <= width));
let showAllScreens = $state(false);
$effect(() => {
const styles = getComputedStyle(document.documentElement);
// Get all computed styles for Tailwind breakpoints
const breakpointPrefix = "--breakpoint-";
screens = Array.from(document.styleSheets)
.flatMap(styleSheet => Array.from(styleSheet.cssRules))
.filter(
(cssRule: CSSRule): cssRule is CSSLayerBlockRule =>
cssRule instanceof CSSLayerBlockRule && cssRule.name === "theme"
)
.flatMap(themeLayer => Array.from(themeLayer.cssRules))
.filter(
(cssRule: CSSRule): cssRule is CSSStyleRule =>
cssRule instanceof CSSStyleRule && cssRule.selectorText.includes(":root")
)
.flatMap(cssRule => Array.from(cssRule.style))
.filter(style => style.startsWith(breakpointPrefix))
.flatMap(breakpoint => {
const size = convertToPixels(styles, styles.getPropertyValue(breakpoint).trim());
return size
? [
{
name: breakpoint.replace(breakpointPrefix, ""),
size
}
]
: [];
})
.sort((a, b) => a.size - b.size);
});
</script>
<div
class="font-mono text-xs font-medium text-white *:fixed *:border *:border-gray-500 *:bg-black *:px-2.5 *:py-1 *:shadow-lg *:shadow-black/50"
>
<div class="bottom-5 left-5 z-50 flex items-center rounded-full !pr-1">
<span>
{width.toLocaleString()} x {height.toLocaleString()}
</span>
{#if matchingScreen}
<div class="mr-1 ml-1.5 h-4 w-px bg-gray-800"></div>
<button
type="button"
class="inline cursor-pointer rounded-l-sm rounded-r-full px-1 hover:bg-neutral-500 active:bg-neutral-600"
onclick={() => (showAllScreens = !showAllScreens)}
>
{matchingScreen.name.toUpperCase()}
</button>
{:else}
<div class="mr-1"></div>
{/if}
</div>
{#if showAllScreens}
<div class="bottom-12 left-5 z-40 rounded-xl duration-300" transition:slide>
{#each screens as screen}
<div
class={[
"flex justify-between gap-6",
{
"font-bold": screen.name === matchingScreen?.name,
"opacity-75": screen.name !== matchingScreen?.name
}
]}
>
<span>{screen.name.toUpperCase()}</span>
<span class="text-neutral-400">{screen.size.toLocaleString()}px</span>
</div>
{/each}
</div>
{/if}
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment