Skip to content
Astro as islands 4 min read

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:media controls 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

QueryHydrates 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.

DirectiveHydrates whenBest for
client:loadImmediately on page loadCritical, always-interactive UI
client:idleBrowser becomes idleLow-priority but eventually-needed UI
client:visibleElement scrolls into viewBelow-the-fold widgets
client:mediaA media query matchesBreakpoint- or device-specific UI
client:onlyAlways, 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:media to 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:media for 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:only only when a component truly cannot render on the server, since you then lose the static fallback.
Last updated June 14, 2026
Was this helpful?