How does shadcn implement Toggle Layout feature?

main
d07a7afchore: add bundui to directory (#9280)
This post might have stale content, as main is 5 commits ahead.

I want to understand exactly how shadcn frontend is doing the feature to "Toggle Layout" to switch between contained / full screen layout in larger screens. There's a button to do it in larger breakpoints, At the left side of the theme toggle

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

I'll help you understand how the "Toggle Layout" feature works. Let me start by exploring the repository structure to find the relevant code.

Let me check the main website/app in the apps directory:

Great! I can see there's a use-layout.tsx hook. Let me also search for components that might handle the layout toggle:

Perfect! Let me check the site-config component:

Now let me find where the SiteConfig component is used and how it's displayed in the header:

Let me search more broadly:

Great! Let me check the site-header component to see how it's positioned:

Now let me check how the layout is actually applied at the root level. Let me look at the main layout and the global CSS:

Let me see the rest of the CSS, particularly how the layout classes are styled:

Let me continue reading the CSS to find the layout-specific styling:

Let me search for where the actual layout styling happens. Let me look at the tailwind config:

Let me check the components themselves that use the layout. Let me search for how the layout affects the page layout:

Perfect! Now I can see the pattern. Let me look at one more component to understand better how 3xl:fixed: works. Let me check if there's any tailwind config:

Let me check the tailwind-indicator component to see if it gives clues, and also look for where the custom variant is defined:

Excellent! Now I have all the pieces. Let me create a comprehensive summary document for the user. Let me also verify the understanding by looking at one more component to see how it's used in practice:

Perfect! Now I have a complete understanding. Let me provide a comprehensive explanation to the user:

##Summary: How shadcn-ui Implements "Toggle Layout" Feature

I've thoroughly explored the source code and can explain exactly how the layout toggle feature works. Here's the complete breakdown:

###1. The Toggle Button Component (site-config.tsx)

The button itself is simple and straightforward:

export function SiteConfig({ className }: React.ComponentProps<typeof Button>) {
  const { layout, setLayout } = useLayout()

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => {
        const newLayout = layout === "fixed" ? "full" : "fixed"
        setLayout(newLayout)
        trackEvent({
          name: "set_layout",
          properties: { layout: newLayout },
        })
      }}
      className={cn("size-8", className)}
      title="Toggle layout"
    >
      <span className="sr-only">Toggle layout</span>
      <GalleryHorizontalIcon />
    </Button>
  )
}

Key points:

  • Uses the useLayout() hook to get the current layout state
  • Toggles between "fixed" and "full" layouts
  • Positioned in the header with 3xl:fixed:flex hidden (only visible at 3xl breakpoint)
  • Positioned to the left of the theme toggle (ModeSwitcher)

###2. The Layout Context & Hook (use-layout.tsx)

This is the core state management system:

State values:

  • "fixed" - Contained/fixed layout
  • "full" - Full-screen layout (default)

How it works:

const Layout = ({
  forcedLayout,
  storageKey = "layout",      // Persists to localStorage
  defaultLayout = "full",     // Default is full-screen
  attribute = "class",        // Applies to HTML element
  value,                      // Optional custom class mapping
  children,
}: LayoutProviderProps) => {
  // 1. Reads from localStorage on mount
  const [layout, setLayoutState] = React.useState<Layout>(() => {
    if (isServer) return defaultLayout
    const saved = localStorage.getItem(storageKey)
    return (saved === "fixed" || saved === "full") ? saved : defaultLayout
  })

  // 2. Applies the layout to the HTML element
  const applyLayout = React.useCallback((layout: Layout) => {
    const name = value ? value[layout] : `layout-${layout}`
    const d = document.documentElement
    
    // Removes opposite class and adds current one
    d.classList.remove(...attrs)  // attrs = ["layout-fixed", "layout-full"]
    if (name) d.classList.add(name)
  }, [])

  // 3. Persists changes and syncs across tabs
  const setLayout = React.useCallback((value: Layout | ((prev: Layout) => Layout)) => {
    // ... update state and save to localStorage
    saveToLS(storageKey, newLayout)
  }, [storageKey])

  // Listen for storage events (other tabs changing layout)
  React.useEffect(() => {
    window.addEventListener("storage", handleStorage)
  }, [])

  // Apply on mount and changes
  React.useEffect(() => {
    applyLayout(forcedLayout ?? layout)
  }, [forcedLayout, layout, applyLayout])

  return (
    <LayoutContext.Provider value={{layout, setLayout, forcedLayout}}>
      {children}
    </LayoutContext.Provider>
  )
}

###3. Initial Layout Application (app/layout.tsx)

To prevent layout shift on hydration, the layout class is applied in a script that runs before React hydrates:

<script
  dangerouslySetInnerHTML={{
    __html: `
      try {
        // ... theme detection code ...
        if (localStorage.layout) {
          document.documentElement.classList.add('layout-' + localStorage.layout)
        }
      } catch (_) {}
    `,
  }}
/>

This ensures the correct layout class (layout-fixed or layout-full) is applied immediately, preventing a flash of unstyled content.

###4. CSS Custom Variant (styles/globals.css)

The magic happens with Tailwind's custom variant system:

@custom-variant fixed (&:is(.layout-fixed *));

This creates a fixed: modifier that matches styles only when the HTML element has the .layout-fixed class.

###5. How Components Use the Layout

With this variant, components can conditionally apply styles using the 3xl:fixed: prefix (3xl = 3xl breakpoint, fixed = the custom variant):

// In site-header.tsx
<div className="3xl:fixed:container flex h-(--header-height)">
  {/* When at 3xl breakpoint AND layout is "fixed", apply "container" class */}
</div>

// In docs-sidebar.tsx
<SidebarProvider className="3xl:fixed:container 3xl:fixed:px-3 ...">
  {/* Multiple utilities can use the fixed: variant */}
</SidebarProvider>

###6. What Actually Changes

The 3xl:fixed: utilities apply container constraints and padding when:

  • Viewport is at 3xl breakpoint (1600px+)
  • AND the layout is set to "fixed"

This makes the content contained within a max-width container instead of spanning the full screen width.

Examples of controlled elements:

  • 3xl:fixed:container - Applies max-width container
  • 3xl:fixed:px-0 - Removes padding in fixed layout
  • 3xl:fixed:bg-none - Removes background
  • 3xl:fixed:max-w-48 - Sets max widths

###Key Features

Persisted: Saves to localStorage, persists across sessions
Cross-tab sync: Uses storage events to sync between tabs
No hydration mismatch: Applied before React hydrates
Responsive: Only active at 3xl breakpoint and above
Tracked: Analytics events capture layout changes
Accessible: Button has ARIA label and semantic structure

The design is elegant: rather than changing the entire layout structure, it uses CSS variants to selectively apply layout utilities only when needed!

Avatar of JoyboyJoyboycommented

Can you show me exactly how it persist the layout state?


REPLY IN THREAD
Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

Absolutely! Let me show you the exact persistence mechanism:

##Exact Persistence Flow

Here's exactly how the layout state is persisted:

###1. Save Function (Lines 24-29)

const saveToLS = (storageKey: string, value: string) => {
  try {
    localStorage.setItem(storageKey, value)
  } catch {
    // Unsupported - handles cases where localStorage is not available
  }
}

Simple wrapper around localStorage.setItem() with error handling.


###2. Reading from Storage on Mount (Lines 48-56)

const [layout, setLayoutState] = React.useState<Layout>(() => {
  if (isServer) return defaultLayout  // SSR check
  try {
    const saved = localStorage.getItem(storageKey)  // storageKey defaults to "layout"
    if (saved === "fixed" || saved === "full") {
      return saved  // ✅ Restore saved layout
    }
    return defaultLayout  // Fallback to default
  } catch {
    return defaultLayout  // Fallback on error
  }
})

On component mount, it immediately checks localStorage.getItem("layout") and restores the user's previous choice.


###3. Saving When Layout Changes (Lines 79-91)

When the user clicks the toggle button, it calls setLayout():

const setLayout = React.useCallback(
  (value: Layout | ((prev: Layout) => Layout)) => {
    if (typeof value === \"function\") {
      setLayoutState((prevLayout) => {
        const newLayout = value(prevLayout)
        saveToLS(storageKey, newLayout)  // ✅ SAVE TO localStorage
        return newLayout
      })
    } else {
      setLayoutState(value)
      saveToLS(storageKey, value)  // ✅ SAVE TO localStorage
    }
  },
  [storageKey]
)

Two paths:

  • Function: When you pass a function like setLayout(prev => prev === "fixed" ? "full" : "fixed")
  • Direct value: When you pass a string like setLayout("fixed")

Both paths call saveToLS() to persist to localStorage.


###4. Cross-Tab Sync (Lines 93-107)

React.useEffect(() => {
  const handleStorage = (e: StorageEvent) => {
    if (e.key !== storageKey) return  // Only listen for "layout" key

    if (!e.newValue) {
      setLayout(defaultLayout)  // If cleared, reset to default
    } else if (e.newValue === \"fixed\" || e.newValue === \"full\") {
      setLayoutState(e.newValue)  // ✅ UPDATE STATE FROM OTHER TAB
    }
  }

  window.addEventListener(\"storage\", handleStorage)  // Browser storage event
  return () => window.removeEventListener(\"storage\", handleStorage)
}, [setLayout, storageKey, defaultLayout])

This is the magic for cross-tab sync:

  • When you toggle layout in one tab, it calls localStorage.setItem("layout", newValue)
  • All OTHER tabs listening to the "storage" event are notified
  • They update their React state immediately without a page reload

###5. Hydration Safety (Lines 121-125)

// Prevent layout changes during hydration
const [isHydrated, setIsHydrated] = React.useState(false)
React.useEffect(() => {
  setIsHydrated(true)
})

The provider value only uses the restored layout after hydration completes to avoid mismatches between server and client.


##Complete Flow Example

User opens the site for the first time:

  1. Initial script in layout.tsx runs before React hydrates
  2. Script checks localStorage.getItem("layout") - returns null
  3. Falls back to default "full" layout
  4. React mounts and sets up the Layout provider
  5. useLayoutState() initializer runs, checks localStorage again → "full"
  6. Context value provided with layout: "full"

User clicks toggle button:

  1. SiteConfig component calls setLayout("fixed")
  2. setLayout() calls saveToLS("layout", "fixed")
  3. localStorage.setItem("layout", "fixed") executed
  4. React state updates to "fixed"
  5. applyLayout("fixed") runs, adds layout-fixed class to <html>
  6. All 3xl:fixed: utilities now activate

User refreshes the page:

  1. Script in layout.tsx runs before React loads
  2. Script reads localStorage.getItem("layout")"fixed"
  3. Script adds layout-fixed class immediately
  4. No flash because styling is applied before content renders
  5. React mounts and finds the same layout state in localStorage
  6. Everything is consistent

User opens another tab:

  1. They toggle layout in Tab A → localStorage.setItem("layout", "full")
  2. Tab B receives storage event with e.key === "layout" and e.newValue === "full"
  3. Tab B's handleStorage() runs → setLayoutState("full")
  4. Tab B's UI updates immediately without page reload

##Storage Key Details

The default storage key is "layout", which you can customize:

<LayoutProvider storageKey="my-custom-layout-key">
  {children}
</LayoutProvider>

This stores data in localStorage as:

localStorage.layout = "fixed"  // or "full"

END OF POST

Log in to add a comment
Claude Haiku 4.5
How does shadcn implement Toggle Layout feature? — shadcn-ui/ui