Responsive Layout
Responsive Layout lets a component respond to the current screen size at runtime, for the cases plain CSS can't handle (like rendering a different component or changing a prop).
useResponsiveLayoutis the hook you reach for. It returns three flags,isMobile,isTablet, andisDesktop, with exactly onetrueat a time.ResponsiveLayoutProvideris optional. It overrides the default breakpoints for the components beneath it.
For the principles behind responsiveness in the design system, including the scaling model and what adapts on its own, see Responsive Design.
import { useResponsiveLayout, ResponsiveLayoutProvider } from '@adaptavant/eds-core';
Reading the current screen size
Call useResponsiveLayout inside any client component to find out which screen size the viewport currently falls into. It returns three booleans, and exactly one is true at any time.
| Return value | When it's true |
|---|---|
isMobile | viewport <= 767px |
isTablet | viewport 768px to 1023px |
isDesktop | viewport > 1023px (also the server-side default) |
Conditional rendering
Read the flags and render accordingly. Here the label changes with the current breakpoint.
Resize the window to watch the value update in real time.
const { isMobile, isTablet, isDesktop } = useResponsiveLayout();
const label = isMobile
? '📱 Mobile'
: isTablet
? '💻 Tablet'
: '🖥️ Desktop';
return (
<Box className="flex items-center gap-2">
<Text>Current screen size:</Text>
<Text className="font-strong">{label}</Text>
</Box>
);
Switching component props
When the markup is same but only a prop should change, set that prop with the flags.
const { isDesktop } = useResponsiveLayout();
const size = isDesktop ? 'standard' : 'large';
return (
<Button size={size} variant="accentPrimary">
Continue
</Button>
);
Customizing breakpoints with the provider
Use ResponsiveLayoutProvider when the default breakpoints don't fit. Place it once, high in your tree (typically wrapping the design system's Root) to apply your breakpoints across the whole app, or lower to scope them to a specific area.
Each value is the inclusive max-width (in px) for that bucket, so { mobile: 599, tablet: 1279 } means ≤ 599 mobile, 600 to 1279 tablet, ≥ 1280 desktop.
A real use case for this is running inside a WebView on a large tablet (wider than 1024px), which would otherwise be treated as Desktop. Setting tablet: 9999 forces every width below it to resolve as Tablet. Apply the same value in your Tailwind screens config so CSS and the hook stay in agreement.
See Responsive Design to learn more.
function Badge() {
const { isMobile, isTablet } = useResponsiveLayout();
const label = isMobile ? 'mobile' : isTablet ? 'tablet' : 'desktop';
return <Text className="font-strong">{label}</Text>;
}
return (
<ResponsiveLayoutProvider breakpoints={{ mobile: 599, tablet: 1279 }}>
<Box className="flex items-center gap-2">
<Text>Breakpoint (mobile ≤ 599, tablet 600 to 1279, desktop ≥ 1280):</Text>
<Badge />
</Box>
</ResponsiveLayoutProvider>
);
Server-side rendering
useResponsiveLayout is safe to call in server-rendered apps. On the server there is no window.matchMedia, so the hook falls back to a stable default and returns isDesktop: true. The real viewport is resolved on the client after hydration.
// Server render and first client paint:
// { isMobile: false, isTablet: false, isDesktop: true }
//
// After hydration, on a 375px-wide phone:
// { isMobile: true, isTablet: false, isDesktop: false }
const { isMobile, isDesktop } = useResponsiveLayout();
What this means in practice:
- Don't gate critical or SEO-relevant content on the flags. Content that only renders when
isMobileistrueis absent from the server HTML and the first paint, then appears after hydration. That means a layout shift, and it stays invisible to crawlers. - Expect a brief desktop-first frame on small viewports before the client corrects it. For purely visual differences, prefer CSS media queries or Tailwind responsive utilities, which render correctly during SSR with no shift.
- No provider is required for SSR. The default is built in. Use
ResponsiveLayoutProvideronly when you need to change the breakpoint values.
API Reference
ResponsiveLayoutProvider
You only need this provider when the default breakpoints don't fit your app. It overrides the breakpoints for every useResponsiveLayout call rendered beneath it.
| Prop | Default | Description |
|---|---|---|
children | _ | React.ReactNodeThe components that should read your custom breakpoints. |
breakpoints? | { mobile: 767, tablet: 1023 } | BreakpointConfigYour breakpoint thresholds, as inclusive max-widths in pixels. |
Best practices
Do
Keep critical content out of breakpoint branches
Render essential and SEO-relevant content unconditionally, and use the flags only to enhance or rearrange it. This keeps content present in the server HTML and the first paint.
const { isMobile } = useResponsiveLayout();
return (
<Box>
<Heading>Pricing</Heading>
{isMobile ? <PricingAccordion /> : <PricingTable />}
</Box>
);
Don’t
Don't gate essential content on client-only flags
On the server the flags resolve to desktop, so anything rendered only when isMobile is true is missing from the initial HTML. It pops in after hydration, causing a layout shift, and stays invisible to crawlers.
const { isMobile } = useResponsiveLayout();
// Heading is absent from SSR + first paint on mobile.
return isMobile ? <Heading>Pricing</Heading> : null;
Do
Use CSS for purely visual responsiveness
For show/hide, spacing, sizing, and layout that doesn't change the component tree, use Tailwind responsive utilities. They work during SSR with no hydration shift and no JavaScript.
<Box className="flex flex-col gap-2 md:flex-row md:gap-4">
<Button>Save</Button>
<Button>Cancel</Button>
</Box>
Don’t
Don't read flags to toggle styles CSS can handle
Reaching for the hook to switch class names on a purely visual change adds a JavaScript dependency and a desktop-first flash before hydration, with no benefit over a media query.
const { isMobile } = useResponsiveLayout();
return (
<Box className={isMobile ? 'flex flex-col gap-2' : 'flex flex-row gap-4'}>
<Button>Save</Button>
<Button>Cancel</Button>
</Box>
);