Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
  1. Home
  2. Blog
  3. Mobile UI in React
  4. Shadcn Sidebar Mobile: Fix the Sheet and Add a Tab Bar
Auf Deutsch lesen
  1. Blog
  2. Mobile UI in React
  3. Shadcn Sidebar Mobile: Fix the Sheet and Add a Tab Bar
Mobile UI in React
Mobile UI in React

Shadcn Sidebar Mobile: Fix the Sheet and Add a Tab Bar

SR
Serban Rusu · Founder of Wingo UI
Nov 5, 2026 · 9 min read

On this page

0%
  1. How does the shadcn sidebar work on mobile?
  2. Why doesn't the shadcn sidebar close when you tap a link?
  3. How do you fix the missing DialogTitle warning?
  4. What else breaks the shadcn sidebar on phones?
    1. The sheet reopens after a rotation
    2. The first render thinks it is on desktop
    3. The page is clipped or the toolbar never hides
    4. The shadcn sidebar collapsible icon mode does nothing on phones
  5. Why pair the sidebar with a bottom tab bar on phones?
  6. How do you build a sidebar that becomes a drawer plus a tab bar?
    1. 1. Install it
    2. 2. Share one link component
    3. 3. Define the navigation once and add a More tab
    4. 4. Read the collapsed cookie on the server
  7. What does the sidebar look like inside the drawer?
  8. Which App Shell props matter on phones?
  9. What should you do next?
  10. FAQ
    1. Why doesn't the shadcn sidebar close when I click a link on mobile?
    2. How do I fix the DialogContent requires a DialogTitle warning in the shadcn sidebar?
    3. Does collapsible icon work on mobile in the shadcn sidebar?
    4. At what width does the shadcn sidebar switch to mobile?
    5. Should a mobile app use a sidebar drawer or a bottom tab bar?

TL;DR

Below 768px the shadcn sidebar renders inside a Sheet with its own openMobile state, and nothing closes that sheet when you tap a link, so call setOpenMobile(false) in each link's onClick. Keep a visually hidden SheetTitle in the mobile sheet, reset openMobile when the breakpoint flips, and let the document scroll instead of putting h-screen on the inset. For a phone layout that feels native, pair the drawer with a bottom tab bar for the three to five most used pages.

Published Nov 5, 2026

The shadcn sidebar mobile experience is where most admin layouts start to feel broken. On a laptop it collapses to icons, remembers its state and responds to Cmd+B. On a phone it slides in as a sheet, you tap a link, the page changes behind it, and the sheet stays open on top. Older copies also log a missing DialogTitle warning, and the last row of the page hides behind Safari's toolbar. This tutorial explains why each of those happens, fixes them in stock shadcn/ui, and then builds an app shell that turns the sidebar into a drawer plus a bottom tab bar below 768px.

How does the shadcn sidebar work on mobile?

It renders a different tree. As of November 2026 the shadcn/ui sidebar source (opens in a new tab) checks isMobile from its useIsMobile hook, and when it is true it returns a Radix-based Sheet instead of the fixed column:

  • The breakpoint is 768px (MOBILE_BREAKPOINT = 768 in hooks/use-mobile.ts).
  • The sheet is 18rem wide (SIDEBAR_WIDTH_MOBILE), the desktop column 16rem, the icon rail 3rem.
  • The sheet has its own state. SidebarProvider keeps open for desktop and a separate openMobile for the sheet. Your open and onOpenChange props on the provider control the desktop state only.
  • toggleSidebar, which SidebarTrigger and Cmd/Ctrl+B call, flips openMobile on phones and open on desktop.
  • collapsible="none" skips the mobile branch entirely: that sidebar renders as a plain column at every width, with no sheet.

That split explains most of the bugs people report as "shadcn sidebar not working on mobile". Code that closes the sidebar with setOpen(false) does nothing on a phone, and a controlled open prop never reaches the sheet.

Why doesn't the shadcn sidebar close when you tap a link?

Nothing in the component closes it, which is the bug behind searches like "shadcn sidebar close on click mobile". In the same source file, openMobile only changes through setOpenMobile, which runs from the trigger, the Cmd+B handler and the Sheet's onOpenChange (the overlay tap and Escape). SidebarMenuButton is a styled button with no close logic, and there is no effect that watches the route.

The reliable fix is to close it in the link's click handler:

tsx
"use client"
import type { ElementType } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar"
type Item = { title: string; url: string; icon: ElementType }
export function NavMain({ items }: { items: Item[] }) {
const pathname = usePathname()
const { isMobile, setOpenMobile } = useSidebar()
return (
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.url}>
<SidebarMenuButton asChild isActive={pathname === item.url}>
<Link href={item.url} onClick={() => isMobile && setOpenMobile(false)}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
)
}

A common alternative is an effect keyed on usePathname() that calls setOpenMobile(false). We prefer the click handler for two reasons. A tap on the page you are already on does not change the pathname, so the effect never fires and the sheet stays open. And on a slow route without a loading.tsx, the pathname changes only once the server responds, so the sheet sits open over the old page in the meantime. Closing on tap gives instant feedback whatever the route does.

How do you fix the missing DialogTitle warning?

Add a visually hidden SheetHeader with a title. The shadcn sidebar sheet is a Radix dialog, and Radix logs "DialogContent requires a DialogTitle" when a dialog has no title, because a screen reader has nothing to announce. shadcn-ui issue #5746 (opens in a new tab), opened on November 6, 2024, reports exactly this for the sidebar.

The current source already contains a hidden header. If your components/ui/sidebar.tsx was generated earlier, the mobile branch probably lacks it. Add it inside SheetContent:

tsx
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={{ "--sidebar-width": SIDEBAR_WIDTH_MOBILE } as React.CSSProperties}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Navigation</SheetTitle>
<SheetDescription>Links to every section of the app.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>

Import SheetHeader, SheetTitle and SheetDescription from @/components/ui/sheet at the top of the file. Pick a title a screen reader user would understand: "Navigation" or your app's name says more than the stock "Sidebar".

What else breaks the shadcn sidebar on phones?

The sheet reopens after a rotation

Nothing resets openMobile when isMobile flips. Open the sheet on a phone, rotate to landscape (most current phones are wider than 768px there), rotate back, and the sheet is open again. One effect inside the provider fixes it:

tsx
"use client"
import { useEffect } from "react"
import { useSidebar } from "@/components/ui/sidebar"
export function ResetMobileSidebar() {
const { isMobile, setOpenMobile } = useSidebar()
useEffect(() => {
if (!isMobile) setOpenMobile(false)
}, [isMobile, setOpenMobile])
return null
}

Render <ResetMobileSidebar /> anywhere inside SidebarProvider.

The first render thinks it is on desktop

useIsMobile starts as undefined, returns false, and measures the window in an effect. The desktop column is hidden with CSS below md, so the sidebar itself does not flash. Anything you branch on isMobile for visible markup does: a header that renders a different button on phones will swap after hydration. Switch visible layout with Tailwind breakpoints and keep the hook for behavior. Our SSR-safe use mobile hook tutorial compares the versions.

The page is clipped or the toolbar never hides

Let the document scroll. To keep the sidebar in place, a common pattern puts h-screen overflow-y-auto on SidebarInset. That turns the inset into its own scroll container. On iOS Safari 100vh is the height with the toolbar hidden, so the last rows sit under the toolbar, and because the window never scrolls, the toolbar never minimizes. The shadcn wrapper already uses min-h-svh and the desktop column is h-svh and fixed, so you can let the document scroll and make your header sticky top-0. The 100vh on mobile guide explains svh, dvh and lvh in detail.

The shadcn sidebar collapsible icon mode does nothing on phones

collapsible="icon" only shapes the desktop column: a 3rem rail with tooltips. Below 768px the sidebar always opens as the 18rem sheet with full labels. That is correct for touch, since tooltips need hover, but it means the rail is not a phone navigation pattern.

Why pair the sidebar with a bottom tab bar on phones?

Because the pages people open every day should sit where the thumb already is. A drawer hides navigation behind a tap on a 44px button in the top corner, the hardest spot to reach with one hand. Apps people open every day put their three to five main destinations at the bottom edge and keep the long list in a drawer or a More tab. shadcn/ui has no tab bar component, so you would build one yourself: a fixed nav with md:hidden, pb-[env(safe-area-inset-bottom)] for the home indicator, an active state from the pathname, and bottom padding on the page so the last row is not covered. Our React bottom navigation bar tutorial covers that build and its edge cases.

How do you build a sidebar that becomes a drawer plus a tab bar?

The Wingo UI App Shell does the layout switch for you. From 768px up it renders the Sidebar as a column that folds into a 64px icon rail (Mod+B, remembered in a cookie). Below 768px the same sidebar moves into a drawer, and the Bottom Tab Bar appears at the bottom edge. The drawer has a visually hidden title, closes on navigation, Escape, a tap on the dimmed page or a swipe back, and returns focus to the menu button. The layout switch is CSS, so the server HTML matches the phone. The files land in your repo as source, styled with Tailwind v4 and your own tokens.

Press Mod+B or the panel button to fold the sidebar into an icon rail; on a phone, tap the menu button, then a link, and the drawer closes as the page changes
$ npx wingo-ui@latest add app-shell
ProApp Shell docs

1. Install it

bash
npx wingo-ui@latest add app-shell

The command also installs the sidebar, top bar, bottom tab bar and sheet it uses.

2. Share one link component

Every nav part reads the router from a LinkProvider. That is also how the drawer knows a navigation started, so it closes on the tap without a pathname effect:

tsx
// app/providers.tsx
"use client"
import type { ReactNode } from "react"
import Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
import { LinkProvider } from "@/lib/navigation"
export function Providers({ children }: { children: ReactNode }) {
const router = useRouter()
return (
<LinkProvider component={Link} currentPath={usePathname()} navigate={router.push}>
{children}
</LinkProvider>
)
}

3. Define the navigation once and add a More tab

tsx
// app/(app)/app-frame.tsx
"use client"
import { useState, type ReactNode } from "react"
import { FileText, House, Inbox, LayoutDashboard, Menu, Settings, Users } from "lucide-react"
import { AppShell } from "@/components/ui/app-shell"
import { Sidebar, type SidebarNavGroup } from "@/components/ui/sidebar"
import { Topbar } from "@/components/ui/topbar"
import type { NavItem } from "@/lib/navigation"
const nav: SidebarNavGroup[] = [
{
items: [
{ label: "Home", href: "/", icon: <LayoutDashboard /> },
{ label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
],
},
{
label: "Sales",
items: [
{ label: "Clients", href: "/clients", icon: <Users /> },
{ label: "Invoices", href: "/invoices", icon: <FileText /> },
],
},
{ label: "Account", items: [{ label: "Settings", href: "/settings", icon: <Settings /> }] },
]
export function AppFrame({ defaultCollapsed, children }: { defaultCollapsed: boolean; children: ReactNode }) {
const [menuOpen, setMenuOpen] = useState(false)
const tabs: NavItem[] = [
{ label: "Home", href: "/", icon: <House /> },
{ label: "Clients", href: "/clients", icon: <Users /> },
{ label: "Inbox", href: "/inbox", icon: <Inbox />, badge: 4 },
{ id: "more", label: "More", icon: <Menu />, onSelect: () => setMenuOpen(true) },
]
return (
<AppShell
defaultCollapsed={defaultCollapsed}
mobileOpen={menuOpen}
onMobileOpenChange={setMenuOpen}
sidebar={<Sidebar groups={nav} filter />}
topbar={<Topbar title="Summit CRM" />}
tabs={tabs}
labels={{ navigation: "Main menu" }}
>
{children}
</AppShell>
)
}

The More tab has no href, so it acts as a button and never becomes the active tab. Controlling mobileOpen lets it open the same drawer the top bar's menu button opens.

4. Read the collapsed cookie on the server

tsx
// app/(app)/layout.tsx
import type { ReactNode } from "react"
import { cookies } from "next/headers"
import { AppFrame } from "./app-frame"
// the shell's default persistKey is "app-shell", so the cookie is "app-shell-collapsed"
export default async function AppLayout({ children }: { children: ReactNode }) {
const stored = (await cookies()).get("app-shell-collapsed")?.value
return <AppFrame defaultCollapsed={stored === "true"}>{children}</AppFrame>
}

Write the cookie name as a string here. persistedCookieName returns the same value, but its module imports React hooks, and importing it into a Server Component fails with "You're importing a module that depends on useInsertionEffect". Reading the cookie in the layout means a collapsed rail is collapsed on the first paint. Stock shadcn needs the same step: its provider writes a sidebar_state cookie but never reads it, so until your layout passes that cookie to defaultOpen, a collapsed sidebar comes back expanded on every full page load.

What does the sidebar look like inside the drawer?

The shell renders a second copy of your sidebar with mode="sheet", mounted only while the drawer is open. In that mode the rows grow to 44px with 15px text, the collapse toggle disappears, and a tap on an in-app link closes the drawer. On desktop, rows are 32 or 36px, and they still grow to 44px on any touch screen, so a tablet in landscape gets usable targets too.

The sidebar in sheet mode, as the drawer shows it on a phone: 44px rows, no collapse button, and a tap on a link closes the drawer
$ npx wingo-ui@latest add sidebar
ProSidebar docs

If you pass your own wrapper component as sidebar, forward its props (mode, variant, collapsible, width, className, style) to the Sidebar, or the drawer copy renders as a desktop column.

Which App Shell props matter on phones?

Four props decide how the phone layout behaves; the defaults suit most apps.

  • Breakpoint. mobileBreakpoint="lg" moves the switch to 1024px, so tablets get the drawer and tab bar too.
  • Scrolling. Keep scroll="window", the default, for apps: the document scrolls, the iOS toolbar minimizes and router scroll restoration works. Use scroll="content" only for embedded apps.
  • Edge swipe. edgeSwipe opens the drawer with a swipe from the start edge. Turn it on in installed PWAs only, since in Safari that edge is the browser's back gesture.
  • Hide on scroll. Pass a BottomTabBar element with hideOnScroll instead of the item array if you want the bar to slide away while reading.

For a full back office in one block, the Admin Shell wires all of this with a workspace switcher, search, notifications, a command menu and a five-tab bar whose More tab opens the drawer. The Next.js admin dashboard tutorial builds a dashboard on it, and more patterns for phones live in the Mobile UI in React category and our mobile-first React components guide.

What should you do next?

If you stay on stock shadcn/ui, add the onClick close, the hidden title and the reset effect above, and remove any h-screen from the inset. Then open your app at 390px and tap through every link. If you want the drawer, rail and tab bar done for you, the App Shell, Sidebar and Bottom Tab Bar are part of Wingo UI Pro; a free account lets you try the free components first.

Components in this post

  • Sidebar

    A React sidebar for admin apps: grouped links with icons, nested sections, badges, a sliding active pill, an icon rail and a filter.

    Pro
  • App Shell

    A React app shell for admin layouts: a collapsible, resizable sidebar, a top bar and the page, with a drawer and a tab bar on phones.

    Pro
  • Admin Shell

    A React admin shell layout with a workspace sidebar, a top bar with search and notifications, a command menu and a bottom tab bar on phones.

    Pro
  • Bottom Tab Bar

    A React bottom tab bar for phones: 3 to 5 tabs at the thumb with an animated pill, badges, a raised center action and hide on scroll.

    Pro

FAQ

Why doesn't the shadcn sidebar close when I click a link on mobile?

The mobile sidebar is a Sheet whose open state, openMobile, only changes through the trigger, the overlay, Escape or setOpenMobile. SidebarMenuButton has no close logic, so call setOpenMobile(false) in the link's onClick when isMobile is true.

How do I fix the DialogContent requires a DialogTitle warning in the shadcn sidebar?

Put a SheetHeader with the sr-only class inside the mobile SheetContent, holding a SheetTitle and a SheetDescription. The current shadcn source already does this; copies generated before that change log the warning until you add it.

Does collapsible icon work on mobile in the shadcn sidebar?

No. collapsible="icon" only shapes the desktop column. Below 768px the sidebar opens as an 18rem sheet with full labels, which is what you want on a touch screen.

At what width does the shadcn sidebar switch to mobile?

At 768px. The useIsMobile hook in shadcn/ui uses a MOBILE_BREAKPOINT of 768 and returns false until it has measured the window after mount.

Should a mobile app use a sidebar drawer or a bottom tab bar?

Both. Put the three to five pages people open every day in a bottom tab bar where the thumb rests, and keep the full navigation in the drawer behind a menu button or a More tab.

Try it in your project

Sidebar

Pro

A React sidebar for admin apps: grouped links with icons, nested sections, badges, a sliding active pill, an icon rail and a filter.

75 components free, no account needed. Pro from $8 a month.

Open Sidebar liveSee pricing
  • Mobile UI
  • Sidebar
  • shadcn/ui
  • Navigation
  • React
  • Next.js

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Mobile UI in React

React Bottom Navigation Bar in Next.js: Safe Areas, Badges

Build a React bottom navigation bar in Next.js: 3 to 5 link tabs, safe areas, badges screen readers announce, hide on scroll, and when a sidebar fits better.

SRSerban Rusu·Oct 9, 2026·8 min read
Mobile UI in React

Nextjs PWA Without next-pwa: Offline Page and App Shell

Build a Nextjs PWA on Next.js 16 without next-pwa: app/manifest.ts, a hand-written offline service worker, an install prompt and an app shell with safe areas.

SRSerban Rusu·Nov 5, 2026·12 min read
Build it: screens and apps

Nextjs Admin Dashboard Tutorial: 5 Screens, Phone-Ready

A nextjs admin dashboard tutorial for the App Router: app shell, KPI home, server-side data tables, settings and team pages, each one usable on a phone.

SRSerban Rusu·Oct 9, 2026·23 min read
Newsletter

Get new posts by email

New posts, product news and occasional offers from Wingo UI. We count clicks to keep it relevant.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • For teams
  • Theme generator

Resources

  • Docs
  • Contact
  • Programs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Compare

  • Wingo UI vs paid kits
  • Wingo UI vs shadcn/ui
  • shadcn/ui alternatives
  • MUI alternatives
  • Tailwind Plus alternatives
  • Magic UI alternatives
  • Free components

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI