How to Make the Layout Responsive Across Different Screen Sizes in refine-shadcn

refine-shadcn implements a fully responsive admin layout by combining CSS media query detection, dynamic panel sizing from react-resizable-panels, and breakpoint-driven sidebar collapse behavior that persists user preferences in cookies.

The refine-shadcn theme package provides a production-ready responsive layout system for React admin applications. By leveraging the DefaultLayout component in packages/theme/src/layouts/default.tsx, you can make the layout responsive across different screen sizes in refine-shadcn without writing custom CSS, thanks to its built-in breakpoint detection and intelligent panel management.

Detecting Viewport Breakpoints with useMediaQuery

The foundation of the responsive system relies on the useMediaQuery hook to detect four distinct viewport sizes: xs (extra small), sm (small), md (medium), and lg (large). These boolean flags drive all layout decisions in the DefaultLayout component.

In packages/theme/src/layouts/default.tsx at lines 38-46, the breakpoints are defined as follows:

const xs = useMediaQuery("only screen and (max-width: 579.999px)");
const sm = useMediaQuery(
  "only screen and (min-width: 640px) and (max-width: 767.999px)",
);
const md = useMediaQuery(
  "only screen and (min-width: 768px) and (max-width: 1023.999px)",
);
const lg = useMediaQuery("only screen and (min-width: 1024px)");

These queries are re-evaluated on every resize event, providing reactive boolean values that determine panel dimensions and sidebar visibility throughout the component lifecycle.

Calculating Dynamic Panel Sizes

The layout uses react-resizable-panels to create a flexible sidebar and content area. To make the layout responsive across different screen sizes in refine-shadcn, the component calculates default panel ratios based on the active breakpoint.

Responsive Layout Arrays

The default layout is computed as a tuple [sidebarSize, contentSize] in lines 55-63 of default.tsx:

const layout = useMemo(() => {
  if (defaultLayout) return defaultLayout;
  if (xs) return [15, 85];
  if (sm) return [20, 80];
  if (md) return [25, 75];
  return [15, 85]; // fallback (lg and up)
}, [defaultLayout, xs, sm, md]);

If the user has previously adjusted the panels, the defaultLayout prop (read from cookies) takes precedence. Otherwise, the sidebar occupies 15% on mobile, 20% on small tablets, 25% on medium screens, and falls back to 15% for large desktops.

To maintain usability, the sidebar enforces different minimum and maximum sizes per breakpoint (lines 66-84):

const SidebarSizes = useMemo(() => {
  if (lg) return { minSize: 11, maxSize: 15 };
  if (md) return { minSize: 15, maxSize: 25 };
  if (sm) return { minSize: 20, maxSize: 30 };
  return { minSize: 15, maxSize: 15 };
}, [sm, md, lg]);

This ensures the navigation panel remains proportional and draggable within sensible bounds across all devices.

Controlling Sidebar Collapse Behavior

The collapsible sidebar automatically adapts to screen real estate while respecting user preferences.

Automatic Mobile Collapse

On extra-small, small, and medium screens, the sidebar collapses automatically to maximize content space. This logic is implemented at lines 47-52 using a derived state:

const [isCollapsed, setIsCollapsed] = useState<boolean>(xs ?? defaultCollapsed);
const hasCollapsed = useMemo(() => isCollapsed || xs || sm || md, [isCollapsed, md, sm, xs]);

The hasCollapsed boolean drives conditional rendering classes that hide navigation text and icons when space is constrained.

User Toggle Persistence

When users manually expand or collapse the sidebar on larger screens, the onExpand and onCollapse callbacks serialize the state to cookies (lines 148-160). The onLayout callback similarly persists panel resize operations:

onLayout={(sizes) => {
  document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`;
}}
onExpand={() => {
  const collapsed = xs;
  setIsCollapsed(collapsed);
  document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(collapsed)}`;
}}
onCollapse={() => {
  const collapsed = true;
  setIsCollapsed(collapsed);
  document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(collapsed)}`;
}}

This cookie-based approach ensures the layout remains consistent across page reloads and browser sessions.

Integrating with Vite and Next.js

The DefaultLayout component consumes these cookies through props, with different implementations for client-side and server-side rendering.

Client-Side Vite-React Setup

In templates/vite-react/src/App.tsx, the application reads cookies using a utility like js-cookie and passes them to the layout component:

import Cookie from "js-cookie";

const layout = Cookie.get("react-resizable-panels:layout");
const collapsed = Cookie.get("react-resizable-panels:collapsed");

<DefaultLayout
  defaultLayout={layout ? JSON.parse(layout) : undefined}
  defaultCollapsed={collapsed ? JSON.parse(collapsed) : false}
  navCollapsedSize={4}
>
  <Outlet />
</DefaultLayout>

Server-Side Next.js Setup

For Next.js applications in templates/nextjs/app/layout.tsx, the root layout reads cookies server-side using the cookies function from next/headers:

import { cookies } from "next/headers";

const cookie = cookies();
const layout = cookie.get("react-resizable-panels:layout");
const collapsed = cookie.get("react-resizable-panels:collapsed");

<AppLayout
  defaultLayout={layout ? JSON.parse(layout.value) : undefined}
  defaultCollapsed={collapsed ? JSON.parse(collapsed.value) : false}
>
  {children}
</AppLayout>

This prevents layout shift during hydration by sending the correct initial state from the server.

Customizing Responsive Behavior

You can extend the default responsive logic to support additional breakpoints or custom persistence mechanisms.

Adding Custom Breakpoints

To add an xl breakpoint for screens wider than 1440px, extend the media queries and layout calculation in default.tsx:

const xl = useMediaQuery("only screen and (min-width: 1440px)");

const layout = useMemo(() => {
  if (defaultLayout) return defaultLayout;
  if (xs) return [15, 85];
  if (sm) return [20, 80];
  if (md) return [25, 75];
  if (lg) return [30, 70];
  if (xl) return [35, 65];
  return [15, 85];
}, [defaultLayout, xs, sm, md, lg, xl]);

Adjusting Collapsed Width

The collapsed sidebar width is controlled via the navCollapsedSize prop. Pass a different pixel value (represented as a percentage of the panel group) when instantiating the layout:

<DefaultLayout
  defaultLayout={defaultLayout}
  defaultCollapsed={defaultCollapsed}
  navCollapsedSize={6}  // Wider collapsed sidebar (default is 4)
>
  {children}
</DefaultLayout>

Using LocalStorage Instead of Cookies

To persist layout state in localStorage rather than cookies, replace the server-side cookie logic with client-side effects. This example from a Next.js root layout initializes state after hydration:

import { useEffect, useState } from "react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const [defaultLayout, setDefaultLayout] = useState<number[]>();
  const [defaultCollapsed, setDefaultCollapsed] = useState<boolean>(false);

  useEffect(() => {
    const layout = localStorage.getItem("react-resizable-panels:layout");
    const collapsed = localStorage.getItem("react-resizable-panels:collapsed");
    setDefaultLayout(layout ? JSON.parse(layout) : undefined);
    setDefaultCollapsed(collapsed ? JSON.parse(collapsed) : false);
  }, []);

  return (
    <html lang="en">
      <body>
        <Suspense fallback={<div>Loading…</div>}>
          <AppLayout
            defaultLayout={defaultLayout}
            defaultCollapsed={defaultCollapsed}
          >
            {children}
          </AppLayout>
        </Suspense>
      </body>
    </html>
  );
}

Summary

To make the layout responsive across different screen sizes in refine-shadcn, the system relies on three coordinated mechanisms:

  • Breakpoint detection via useMediaQuery at lines 38-46 of default.tsx provides reactive viewport flags
  • Dynamic panel sizing using react-resizable-panels calculates responsive [sidebar, content] ratios and enforces min/max constraints per breakpoint
  • Persistent collapse state stores user preferences in cookies, with automatic collapse on mobile devices (xs, sm, md)

The implementation spans the core theme package (packages/theme/src/layouts/default.tsx), the UI primitives (packages/theme/src/ui/resizable.tsx), and framework-specific templates for Vite and Next.js.

Frequently Asked Questions

How does refine-shadcn detect screen size changes?

The DefaultLayout component uses the useMediaQuery hook to evaluate four CSS media queries corresponding to xs, sm, md, and lg breakpoints. These boolean values update automatically when the viewport crosses threshold boundaries, triggering recalculations of panel sizes and collapse states.

Where is the responsive layout logic located in the source code?

All responsive logic resides in packages/theme/src/layouts/default.tsx. This file contains the breakpoint detection (lines 38-46), layout size calculations (lines 55-63), sidebar constraint definitions (lines 66-84), and collapse state management (lines 47-52). The underlying resizable panel wrapper is defined in packages/theme/src/ui/resizable.tsx.

Can I use localStorage instead of cookies for layout persistence?

Yes, though it requires client-side initialization. Replace the cookie reading logic in your application entry point (e.g., App.tsx or layout.tsx) with useEffect hooks that read from localStorage.getItem(), then pass the deserialized values to DefaultLayout via the defaultLayout and defaultCollapsed props.

How do I add a new breakpoint for extra-large screens?

Define a new media query using useMediaQuery (e.g., const xl = useMediaQuery("only screen and (min-width: 1440px)")), then include the xl variable in the dependency arrays of the layout and SidebarSizes useMemo hooks. Add conditional returns for the new breakpoint to specify custom panel percentages and size constraints.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →