Shadcn Sidebar Mobile: Fix the Sheet and Add a Tab Bar
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 = 768inhooks/use-mobile.ts). - The sheet is
18remwide (SIDEBAR_WIDTH_MOBILE), the desktop column16rem, the icon rail3rem. - The sheet has its own state.
SidebarProviderkeepsopenfor desktop and a separateopenMobilefor the sheet. YouropenandonOpenChangeprops on the provider control the desktop state only. toggleSidebar, whichSidebarTriggerand Cmd/Ctrl+B call, flipsopenMobileon phones andopenon 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:
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:
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:
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.
$ npx wingo-ui@latest add app-shell1. Install it
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:
3. Define the navigation once and add a More tab
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
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.
$ npx wingo-ui@latest add sidebarIf 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. Usescroll="content"only for embedded apps. - Edge swipe.
edgeSwipeopens 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
BottomTabBarelement withhideOnScrollinstead 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.
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.
- Mobile UI
- Sidebar
- shadcn/ui
- Navigation
- React
- Next.js

