How do I use matchMedia in svelte?

I have a svelte component here. I plan on making the menu open up whenever I scroll to the top. However, I don't want this listener to work when the window is like.. x pixels or less. In vanilla JS, I would use window.matchMedia, and attach a listener to it, checking when it changes, toggling the menu and removing the listener. What would be the svelte-y way to do so?
4 Replies
clevermissfox
clevermissfox2mo ago
Very new to learning svelte but I just came across a need for this the other day and ended up installing the library
npm install --save-dev svelte-match-media
npm install --save-dev svelte-match-media
Lofty!
Lofty!2mo ago
svelte-match-media has a way to attach event listeners to it? Its docs dont give any examples
clevermissfox
clevermissfox2mo ago
-svelte-match-media
//main.js
import { setup } from 'svelte-match-media';

setup({
desktop: 'screen and (min-width: 769px)',
mobile: 'screen and (max-width: 768px)'
});
//main.js
import { setup } from 'svelte-match-media';

setup({
desktop: 'screen and (min-width: 769px)',
mobile: 'screen and (max-width: 768px)'
});
<script>
import { media } from 'svelte-match-media';
</script>

<main>
{#if $media.desktop}
<nav>Desktop Navigation</nav>
{:else}
<nav>Mobile Navigation</nav>
{/if}
</main>
<script>
import { media } from 'svelte-match-media';
</script>

<main>
{#if $media.desktop}
<nav>Desktop Navigation</nav>
{:else}
<nav>Mobile Navigation</nav>
{/if}
</main>
Alternatively, -svelte-media:
<script>
import watchMedia from 'svelte-media';

// Define your media queries
const mediaqueries = {
small: "(max-width: 849px)",
large: "(min-width: 850px)"
};

// Create a media store
const media = watchMedia(mediaqueries);
</script>

<div>
{#if $media.large}
<p>Large screen detected</p>
{:else}
<p>Small screen detected</p>
{/if}
</div>
<script>
import watchMedia from 'svelte-media';

// Define your media queries
const mediaqueries = {
small: "(max-width: 849px)",
large: "(min-width: 850px)"
};

// Create a media store
const media = watchMedia(mediaqueries);
</script>

<div>
{#if $media.large}
<p>Large screen detected</p>
{:else}
<p>Small screen detected</p>
{/if}
</div>
-svelte-match-media
<script>
import { media } from 'svelte-match-media';
import { onMount } from 'svelte';

let scrollY = 0;

onMount(() => {
const handleScroll = () => {
if (window.scrollY === 0 && $media.desktop) {
// Open menu logic
}
};

// Attach scroll listener
window.addEventListener('scroll', handleScroll);

return () => {
// Clean up listener
window.removeEventListener('scroll', handleScroll);
};
});
</script>
<script>
import { media } from 'svelte-match-media';
import { onMount } from 'svelte';

let scrollY = 0;

onMount(() => {
const handleScroll = () => {
if (window.scrollY === 0 && $media.desktop) {
// Open menu logic
}
};

// Attach scroll listener
window.addEventListener('scroll', handleScroll);

return () => {
// Clean up listener
window.removeEventListener('scroll', handleScroll);
};
});
</script>
From what I’m reading you’d choose -svelte-match-media if you need global access to your media query , and -svelte-media if you only need a one off on one component
Lofty!
Lofty!2mo ago
Oh, I didn't see this last night 😅 I wrote a tiny utility for this and it worked
interface Params {
query: string;
initial: (matches: boolean) => void;
listener: (list: MediaQueryListEvent) => void;
}

export class MediaQuery {
#queryList: MediaQueryList | undefined;
#params: Params;

constructor(params: Params) {
this.#params = params;
}

mountMediaQuery = () => {
this.#queryList = window.matchMedia(this.#params.query);
this.#params.initial(this.#queryList.matches);

const instance = this.#params.listener.bind(this);
this.#queryList.addEventListener("change", instance);
return () => this.#queryList?.removeEventListener("change", instance);
}
}
interface Params {
query: string;
initial: (matches: boolean) => void;
listener: (list: MediaQueryListEvent) => void;
}

export class MediaQuery {
#queryList: MediaQueryList | undefined;
#params: Params;

constructor(params: Params) {
this.#params = params;
}

mountMediaQuery = () => {
this.#queryList = window.matchMedia(this.#params.query);
this.#params.initial(this.#queryList.matches);

const instance = this.#params.listener.bind(this);
this.#queryList.addEventListener("change", instance);
return () => this.#queryList?.removeEventListener("change", instance);
}
}
let ticking: number | boolean = false;
const onScrollListener = () => {
ticking ||= requestAnimationFrame(() => {
burger = window.scrollY === 0;
ticking = false;
});
};

const mediaQuery = new MediaQuery({
query: "(width >= 768px)",
initial(matches: boolean) {
if (matches) document.addEventListener("scroll", onScrollListener);
burger = matches;
},
listener(list: MediaQueryListEvent) {
burger = list.matches;
if (list.matches) document.addEventListener("scroll", onScrollListener);
else document.removeEventListener("scroll", onScrollListener);
},
});

onMount(mediaQuery.mountMediaQuery);
let ticking: number | boolean = false;
const onScrollListener = () => {
ticking ||= requestAnimationFrame(() => {
burger = window.scrollY === 0;
ticking = false;
});
};

const mediaQuery = new MediaQuery({
query: "(width >= 768px)",
initial(matches: boolean) {
if (matches) document.addEventListener("scroll", onScrollListener);
burger = matches;
},
listener(list: MediaQueryListEvent) {
burger = list.matches;
if (list.matches) document.addEventListener("scroll", onScrollListener);
else document.removeEventListener("scroll", onScrollListener);
},
});

onMount(mediaQuery.mountMediaQuery);
Want results from more Discord servers?
Add your server