client:media
The client:media directive hydrates an island only when a given CSS media query evaluates to true in the browser. This makes it the ideal tool for interactivity that is only relevant at certain viewport sizes or under specific device conditions — a desktop-only mega-menu, a mobile-only swipe carousel, or controls that should stay dormant when the user prefers reduced motion. Because Astro ships zero JavaScript by default, client:media lets you push the cost of a component’s JS to only the users who can actually benefit from it.
How it works
Astro renders the component to static HTML at build time (or on request), exactly like every other island. It then emits a tiny inline runtime that registers a window.matchMedia() listener for the query you supply. If the query matches when the page loads, the component’s framework bundle is fetched and the island hydrates. If it does not match, the JavaScript is never downloaded.
You pass the media query as the directive’s value:
---
import Sidebar from "../components/Sidebar.jsx";
import MobileNav from "../components/MobileNav.jsx";
---
<!-- Only ships JS and becomes interactive on wide screens -->
<Sidebar client:media="(min-width: 1024px)" />
<!-- Only ships JS and becomes interactive on narrow screens -->
<MobileNav client:media="(max-width: 1023px)" />
The value is a standard CSS media query string — the same syntax you would use inside an @media rule. Width, height, orientation, pointer capabilities, and user preferences all work.
Important: the static HTML is always rendered regardless of the query.
client:mediacontrols hydration, not rendering. If you want the markup itself to disappear at certain breakpoints, hide it with CSS (display: none) or render conditionally on the server.
Common media queries
| Query | Hydrates when |
|---|---|
(min-width: 768px) | Viewport is at least 768px wide (tablet and up) |
(max-width: 767px) | Viewport is at most 767px wide (mobile) |
(orientation: landscape) | Device is in landscape orientation |
(pointer: fine) | A precise pointer (mouse/trackpad) is available |
(prefers-reduced-motion: no-preference) | The user has not requested reduced motion |
(min-width: 600px) and (max-width: 1199px) | Viewport falls within a mid-range band |
A practical example
Suppose you have an animated hero that is only worth running for users who have not opted into reduced motion. You can gate hydration on that preference so the animation logic is never downloaded for users who would never see it:
---
import AnimatedHero from "../components/AnimatedHero.svelte";
---
<AnimatedHero
client:media="(prefers-reduced-motion: no-preference)"
title="Build faster with islands"
/>
<noscript>
<p>Interactive hero unavailable.</p>
</noscript>
You can confirm the conditional loading in the browser. Open DevTools, throttle to a narrow viewport, and watch the Network panel:
Output:
# Wide viewport (>= 1024px): Sidebar bundle is requested
GET /_astro/Sidebar.[hash].js 200 4.1 kB
# Narrow viewport (< 1024px): no Sidebar request appears
(Sidebar.[hash].js is never fetched)
client:media vs. other directives
client:media is one of several hydration strategies. Choose it specifically when the condition for interactivity is a viewport or device characteristic rather than timing or scroll position.
| Directive | Hydrates when | Best for |
|---|---|---|
client:load | Immediately on page load | Critical, always-interactive UI |
client:idle | Browser becomes idle | Low-priority but eventually-needed UI |
client:visible | Element scrolls into view | Below-the-fold widgets |
client:media | A media query matches | Breakpoint- or device-specific UI |
client:only | Always, client-side only (no SSR) | Components that cannot render on the server |
Things to watch out for
The query is evaluated once on load and Astro re-evaluates it as the match state changes, so resizing the window across the breakpoint will hydrate a previously-dormant island. However, the component’s state starts fresh at hydration — any interaction before hydration is not captured.
Gotcha: do not use
client:mediato swap between two components that render different markup at different sizes and expect the unmatched one to be removed. Both sets of static HTML are present in the DOM. Pair the directive with CSS visibility rules so layout and accessibility stay correct.
Best practices
- Reserve
client:mediafor interactivity that is genuinely irrelevant outside the matched condition — pure JS savings, not a layout tool. - Always pair it with CSS (
display: none/ responsive utilities) so the non-hydrated markup is visually hidden at the right breakpoints. - Prefer preference-based queries like
(prefers-reduced-motion: no-preference)to respect user settings and avoid shipping unused animation code. - Keep the static HTML accessible and meaningful on its own; the island should enhance, not gate, core content.
- Test across the breakpoint boundary by resizing — confirm that hydration fires only when expected in the Network panel.
- Combine with
client:onlyonly when a component truly cannot render on the server, since you then lose the static fallback.