# Shadcn Sidebar Mobile: Sheet reparieren, Tab Bar ergänzen

> Shadcn sidebar mobile reparieren: Sheet beim Navigieren schließen, versteckten Titel ergänzen, Höhen-Bugs beheben und am Handy eine Bottom Tab Bar dazunehmen.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 5. Nov. 2026
- Kategorie: [Mobile UI mit React](https://wingo-ui.com/de/blog/category/mobile-ui)
- Lesezeit: 9 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/shadcn-sidebar-mobile
- Englische Fassung: https://wingo-ui.com/blog/shadcn-sidebar-mobile

## Kurz gesagt

Unter 768px rendert die shadcn Sidebar in einem Sheet mit eigenem openMobile-State, und nichts schließt dieses Sheet, wenn du auf einen Link tippst. Ruf also im onClick jedes Links setOpenMobile(false) auf. Behalte einen visuell versteckten SheetTitle im mobilen Sheet, setz openMobile zurück, wenn der Breakpoint wechselt, und lass das Dokument scrollen, statt h-screen auf das Inset zu setzen. Damit sich das Layout am Handy nativ anfühlt, kombinierst du den Drawer mit einer Bottom Tab Bar für die drei bis fünf meistgenutzten Seiten.

Beim Thema shadcn sidebar mobile fühlen sich die meisten Admin-Layouts zum ersten Mal kaputt an. Am Laptop klappt die Sidebar zu Icons zusammen, merkt sich ihren Zustand und reagiert auf Cmd+B. Am Handy gleitet sie als Sheet herein, du tippst auf einen Link, dahinter wechselt die Seite, und das Sheet bleibt einfach offen. Ältere Kopien loggen zusätzlich eine Warnung wegen eines fehlenden DialogTitle, und die letzte Zeile der Seite verschwindet hinter der Toolbar von Safari. Dieses Tutorial erklärt, woher jedes dieser Probleme kommt, behebt sie im Standard-shadcn/ui und baut dann eine App Shell, die aus der Sidebar unter 768px einen Drawer plus eine Bottom Tab Bar macht.

## Wie funktioniert die shadcn Sidebar auf dem Handy?

Sie rendert einen anderen Baum. Stand November 2026 prüft der [Quellcode der shadcn/ui Sidebar](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/sidebar.tsx) `isMobile` aus seinem Hook `useIsMobile`. Ist der Wert true, gibt er statt der festen Spalte ein `Sheet` auf Basis von Radix zurück:

- Der Breakpoint liegt bei 768px (`MOBILE_BREAKPOINT = 768` in `hooks/use-mobile.ts`).
- Das Sheet ist `18rem` breit (`SIDEBAR_WIDTH_MOBILE`), die Desktop-Spalte `16rem`, die Icon-Leiste `3rem`.
- Das shadcn Sidebar Sheet hat seinen eigenen State. `SidebarProvider` hält `open` für den Desktop und ein separates `openMobile` für das Sheet. Deine Props `open` und `onOpenChange` am Provider steuern nur den Desktop-Zustand.
- `toggleSidebar`, das `SidebarTrigger` und Cmd/Ctrl+B aufrufen, schaltet am Handy `openMobile` um und am Desktop `open`.
- `collapsible="none"` überspringt den mobilen Zweig komplett: Diese Sidebar rendert bei jeder Breite als normale Spalte, ohne Sheet.

Diese Trennung erklärt die meisten Bugs, die als „shadcn sidebar not working on mobile“ gemeldet werden. Code, der die Sidebar mit `setOpen(false)` schließt, bewirkt am Handy nichts, und ein kontrolliertes `open`-Prop erreicht das Sheet nie.

## Warum schließt die shadcn Sidebar nicht, wenn du auf einen Link tippst?

Nichts in der Komponente schließt sie. Das ist der Bug hinter Suchen wie „shadcn sidebar close on click mobile“ oder „shadcn sidebar mobile schließen“. In derselben Quelldatei ändert sich `openMobile` nur über `setOpenMobile`, und das läuft im Trigger, im Handler für Cmd+B und im `onOpenChange` des Sheets (Tipp auf das Overlay und Escape). `SidebarMenuButton` ist ein gestylter Button ohne Logik zum Schließen, und es gibt keinen Effect, der die Route beobachtet.

Der verlässliche Fix: Schließ das Sheet im Click-Handler des Links.

```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>
  )
}
```

Eine verbreitete Alternative ist ein Effect auf `usePathname()`, der `setOpenMobile(false)` aufruft. Wir nehmen lieber den Click-Handler, aus zwei Gründen. Ein Tipp auf die Seite, auf der du schon bist, ändert den Pathname nicht, also feuert der Effect nie, und das Sheet bleibt offen. Und bei einer langsamen Route ohne `loading.tsx` ändert sich der Pathname erst, wenn der Server antwortet. Bis dahin liegt das Sheet offen über der alten Seite. Schließt du schon beim Tippen, gibt es sofort Feedback, egal was die Route macht.

## Wie behebst du die Warnung wegen des fehlenden DialogTitle?

Ergänz einen visuell versteckten `SheetHeader` mit Titel. Das Sheet der shadcn Sidebar ist ein Radix-Dialog, und Radix loggt „DialogContent requires a DialogTitle“, wenn ein Dialog keinen Titel hat, weil ein Screenreader sonst nichts ansagen kann. [Das shadcn-ui Issue #5746](https://github.com/shadcn-ui/ui/issues/5746), eröffnet am 6. November 2024, meldet genau das für die Sidebar.

Der aktuelle Quellcode enthält den versteckten Header schon. Wurde deine `components/ui/sidebar.tsx` früher erzeugt, fehlt er im mobilen Zweig wahrscheinlich. Setz ihn in `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 zu allen Bereichen der App.</SheetDescription>
  </SheetHeader>
  <div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
```

Importier `SheetHeader`, `SheetTitle` und `SheetDescription` oben in der Datei aus `@/components/ui/sheet`. Wähl einen Titel, den jemand mit Screenreader versteht: „Navigation“ oder der Name deiner App sagt mehr als das Standard-„Sidebar“.

## Was geht an der shadcn Sidebar auf Handys sonst noch kaputt?

### Das Sheet öffnet sich nach dem Drehen wieder

Nichts setzt `openMobile` zurück, wenn `isMobile` wechselt. Öffne das Sheet am Handy, dreh ins Querformat (die meisten aktuellen Handys sind dort breiter als 768px), dreh zurück, und das Sheet ist wieder offen. Ein einziger Effect im Provider behebt das:

```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 />` irgendwo innerhalb von `SidebarProvider`.

### Der erste Render hält sich für den Desktop

`useIsMobile` startet mit `undefined`, gibt `false` zurück und misst das Fenster in einem Effect. Die Desktop-Spalte ist unter `md` per CSS ausgeblendet, die Sidebar selbst flackert also nicht. Alles, was du für sichtbares Markup an `isMobile` verzweigst, flackert dagegen schon: Ein Header, der am Handy einen anderen Button rendert, tauscht ihn nach der Hydration aus. Schalte sichtbares Layout mit Tailwind-Breakpoints um und nutz den Hook nur für Verhalten. Unser [Tutorial zum SSR-sicheren use mobile Hook](https://wingo-ui.com/de/blog/react-use-mobile-hook-ssr) vergleicht die Varianten.

### Die Seite wird abgeschnitten oder die Toolbar verschwindet nie

Lass das Dokument scrollen. Damit die Sidebar stehen bleibt, setzt ein verbreitetes Muster `h-screen overflow-y-auto` auf `SidebarInset`. Dadurch wird das Inset zu einem eigenen Scroll-Container. In Safari auf iOS ist `100vh` die Höhe bei ausgeblendeter Toolbar, also liegen die letzten Zeilen unter der Toolbar. Und weil das Fenster nie scrollt, wird die Toolbar nie kleiner. Der shadcn-Wrapper nutzt bereits `min-h-svh`, und die Desktop-Spalte ist `h-svh` und fixiert. Du kannst also das Dokument scrollen lassen und deinen Header `sticky top-0` machen. Der [Guide zu 100vh auf Mobilgeräten](https://wingo-ui.com/de/blog/100vh-mobile-dvh-tailwind) erklärt `svh`, `dvh` und `lvh` im Detail.

### Der Modus shadcn sidebar collapsible icon tut am Handy nichts

`collapsible="icon"` formt nur die Desktop-Spalte: eine 3rem breite Leiste mit Tooltips. Unter 768px öffnet sich die Sidebar immer als 18rem breites Sheet mit vollen Labels. Für Touch ist das richtig, denn Tooltips brauchen Hover. Es heißt aber auch, dass die Icon-Leiste kein Navigationsmuster für Handys ist.

## Warum kombinierst du die Sidebar am Handy mit einer Bottom Tab Bar?

Weil die Seiten, die Leute jeden Tag öffnen, dort liegen sollten, wo der Daumen schon ist. Ein Drawer versteckt die Navigation hinter einem Tipp auf einen 44px-Button in der oberen Ecke, also an der Stelle, die mit einer Hand am schwersten zu erreichen ist. Apps, die Leute täglich öffnen, legen ihre drei bis fünf wichtigsten Ziele an den unteren Rand und lassen die lange Liste in einem Drawer oder einem Mehr-Tab. shadcn/ui hat keine Tab-Bar-Komponente, du müsstest sie also selbst bauen: ein fixiertes `nav` mit `md:hidden`, `pb-[env(safe-area-inset-bottom)]` für den Home-Indikator, einen aktiven Zustand aus dem Pathname und Padding unten auf der Seite, damit die letzte Zeile frei bleibt. Unser [Tutorial zur React Bottom Navigation Bar](https://wingo-ui.com/de/blog/react-bottom-navigation-bar) zeigt diesen Aufbau samt Sonderfällen.

## Wie baust du eine Sidebar, die zu Drawer plus Tab Bar wird?

Die [App Shell](https://wingo-ui.com/components/app-shell) von Wingo UI übernimmt die Umschaltung des Layouts für dich. Ab 768px rendert sie die [Sidebar](https://wingo-ui.com/components/sidebar) als Spalte, die sich zu einer 64px breiten Icon-Leiste zusammenklappen lässt (Mod+B, in einem Cookie gespeichert). Unter 768px wandert dieselbe Sidebar in einen Drawer, und die [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) erscheint am unteren Rand. Der Drawer hat einen visuell versteckten Titel, schließt beim Navigieren, mit Escape, bei einem Tipp auf die abgedunkelte Seite oder wenn du ihn wegwischst, und gibt den Fokus an den Menü-Button zurück. Die Umschaltung läuft über CSS, das Server-HTML passt also zum Handy. Die Dateien landen als Quellcode in deinem Repo, gestylt mit Tailwind v4 und deinen eigenen Tokens.

> Live-Demo (App Shell): Drück Mod+B oder den Panel-Button, um die Sidebar zur Icon-Leiste zu falten; am Handy tippst du auf den Menü-Button, dann auf einen Link, und der Drawer schließt sich, während die Seite wechselt. Probier sie unter [App Shell](https://wingo-ui.com/components/app-shell) aus und installier sie mit `npx wingo-ui@latest add app-shell`.

### 1. Installier sie

```bash
npx wingo-ui@latest add app-shell
```

Der Befehl installiert auch die Sidebar, die Top Bar, die Bottom Tab Bar und das Sheet, die sie nutzt.

### 2. Nutz eine gemeinsame Link-Komponente

Jedes Navigationsteil liest den Router aus einem `LinkProvider`. So weiß auch der Drawer, dass eine Navigation begonnen hat, und schließt sich beim Tippen, ganz ohne Effect auf den Pathname:

```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. Definier die Navigation einmal und ergänz einen Mehr-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: "Start", href: "/", icon: <LayoutDashboard /> },
      { label: "Posteingang", href: "/inbox", icon: <Inbox />, badge: 4 },
    ],
  },
  {
    label: "Vertrieb",
    items: [
      { label: "Kunden", href: "/clients", icon: <Users /> },
      { label: "Rechnungen", href: "/invoices", icon: <FileText /> },
    ],
  },
  { label: "Konto", items: [{ label: "Einstellungen", href: "/settings", icon: <Settings /> }] },
]

export function AppFrame({ defaultCollapsed, children }: { defaultCollapsed: boolean; children: ReactNode }) {
  const [menuOpen, setMenuOpen] = useState(false)

  const tabs: NavItem[] = [
    { label: "Start", href: "/", icon: <House /> },
    { label: "Kunden", href: "/clients", icon: <Users /> },
    { label: "Posteingang", href: "/inbox", icon: <Inbox />, badge: 4 },
    { id: "more", label: "Mehr", 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: "Hauptmenü" }}
    >
      {children}
    </AppShell>
  )
}
```

Der Mehr-Tab hat kein `href`. Er verhält sich also wie ein Button und wird nie zum aktiven Tab. Weil du `mobileOpen` kontrollierst, öffnet er denselben Drawer wie der Menü-Button in der Top Bar.

### 4. Lies das Collapsed-Cookie auf dem Server

```tsx
// app/(app)/layout.tsx
import type { ReactNode } from "react"
import { cookies } from "next/headers"
import { AppFrame } from "./app-frame"

// der Standard-persistKey der Shell ist "app-shell", das Cookie heißt also "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>
}
```

Schreib den Cookie-Namen hier als String. `persistedCookieName` liefert denselben Wert, aber sein Modul importiert React-Hooks, und der Import in eine Server Component scheitert mit „You're importing a module that depends on `useInsertionEffect`“. Liest das Layout das Cookie, ist eine eingeklappte Leiste schon beim ersten Paint eingeklappt. Das Standard-shadcn braucht denselben Schritt: Sein Provider schreibt ein Cookie `sidebar_state`, liest es aber nie. Solange dein Layout dieses Cookie nicht an `defaultOpen` übergibt, ist eine eingeklappte Sidebar nach jedem vollständigen Neuladen der Seite wieder ausgeklappt.

## Wie sieht die Sidebar im Drawer aus?

Die Shell rendert eine zweite Kopie deiner Sidebar mit `mode="sheet"`, die nur gemountet ist, solange der Drawer offen ist. In diesem Modus wachsen die Zeilen auf 44px mit 15px Schrift, der Button zum Einklappen verschwindet, und ein Tipp auf einen Link innerhalb der App schließt den Drawer. Am Desktop sind die Zeilen 32 oder 36px hoch und wachsen auf jedem Touchscreen trotzdem auf 44px. So bekommt auch ein Tablet im Querformat Flächen, die sich gut treffen lassen.

> Live-Demo (Sidebar): Die Sidebar im Sheet-Modus, wie der Drawer sie am Handy zeigt: 44px hohe Zeilen, kein Button zum Einklappen, und ein Tipp auf einen Link schließt den Drawer. Probier sie unter [Sidebar](https://wingo-ui.com/components/sidebar) aus und installier sie mit `npx wingo-ui@latest add sidebar`.

Übergibst du eine eigene Wrapper-Komponente als `sidebar`, reich ihre Props (`mode`, `variant`, `collapsible`, `width`, `className`, `style`) an die `Sidebar` weiter. Sonst rendert die Kopie im Drawer als Desktop-Spalte.

## Welche Props der App Shell zählen am Handy?

Vier Props bestimmen, wie sich das Layout am Handy verhält; die Standardwerte passen für die meisten Apps.

- **Breakpoint.** `mobileBreakpoint="lg"` verschiebt die Umschaltung auf 1024px, dann bekommen auch Tablets Drawer und Tab Bar.
- **Scrollen.** Behalte für Apps `scroll="window"`, den Standard: Das Dokument scrollt, die iOS-Toolbar wird kleiner, und die Scroll-Wiederherstellung des Routers funktioniert. Nimm `scroll="content"` nur für eingebettete Apps.
- **Wischen vom Rand.** `edgeSwipe` öffnet den Drawer mit einer Wischgeste von der Startkante. Schalte es nur in installierten PWAs ein, denn in Safari ist diese Kante die Zurück-Geste des Browsers.
- **Beim Scrollen ausblenden.** Übergib statt des Item-Arrays ein `BottomTabBar`-Element mit `hideOnScroll`, wenn die Leiste beim Lesen wegrutschen soll.

Für ein komplettes Backoffice in einem Block verdrahtet die [Admin Shell](https://wingo-ui.com/components/admin-shell) all das mit Workspace-Switcher, Suche, Benachrichtigungen, einem Befehlsmenü und einer Tab Bar mit fünf Tabs, deren Mehr-Tab den Drawer öffnet. Das [Tutorial zum Next.js Admin-Dashboard](https://wingo-ui.com/de/blog/nextjs-admin-dashboard-tutorial) baut ein Dashboard darauf auf, und weitere Muster fürs Handy findest du in der Kategorie [Mobile UI in React](https://wingo-ui.com/de/blog/category/mobile-ui) und in unserem [Guide zu Mobile-first React-Komponenten](https://wingo-ui.com/de/blog/mobile-first-react-components).

## Was solltest du als Nächstes tun?

Bleibst du beim Standard-shadcn/ui, ergänz das Schließen per `onClick`, den versteckten Titel und den Reset-Effect von oben, und entferne jedes `h-screen` vom Inset. Öffne dann deine App bei 390px und tipp dich durch jeden Link. Willst du Drawer, Icon-Leiste und Tab Bar fertig haben, dann gehören App Shell, Sidebar und Bottom Tab Bar zu [Wingo UI Pro](https://wingo-ui.com/de/pricing); mit einem kostenlosen Konto testest du zuerst die kostenlosen Komponenten.

## Komponenten in diesem Artikel

- [Sidebar](https://wingo-ui.com/components/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. Installation: `npx wingo-ui@latest add sidebar`
- [App Shell](https://wingo-ui.com/components/app-shell) (Pro): 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. Installation: `npx wingo-ui@latest add app-shell`
- [Admin Shell](https://wingo-ui.com/components/admin-shell) (Pro): 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. Installation: `npx wingo-ui@latest add admin-shell`
- [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) (Pro): 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. Installation: `npx wingo-ui@latest add bottom-tab-bar`

## Häufige Fragen

### Warum schließt die shadcn Sidebar auf dem Handy nicht, wenn ich auf einen Link klicke?

Die mobile Sidebar ist ein Sheet, dessen offener Zustand openMobile sich nur über den Trigger, das Overlay, Escape oder setOpenMobile ändert. SidebarMenuButton hat keine Logik zum Schließen, also ruf setOpenMobile(false) im onClick des Links auf, wenn isMobile true ist.

### Wie behebe ich die Warnung „DialogContent requires a DialogTitle“ in der shadcn Sidebar?

Setz in das mobile SheetContent einen SheetHeader mit der Klasse sr-only, der einen SheetTitle und eine SheetDescription enthält. Der aktuelle shadcn-Quellcode macht das bereits; Kopien, die vor dieser Änderung erzeugt wurden, loggen die Warnung, bis du ihn ergänzt.

### Funktioniert collapsible icon auf dem Handy in der shadcn Sidebar?

Nein. collapsible="icon" formt nur die Desktop-Spalte. Unter 768px öffnet sich die Sidebar als 18rem breites Sheet mit vollen Labels, und genau das willst du auf einem Touchscreen.

### Ab welcher Breite wechselt die shadcn Sidebar in den mobilen Modus?

Ab 768px. Der Hook useIsMobile in shadcn/ui nutzt einen MOBILE_BREAKPOINT von 768 und gibt false zurück, bis er das Fenster nach dem Mount gemessen hat.

### Sollte eine mobile App einen Sidebar-Drawer oder eine Bottom Tab Bar nutzen?

Beides. Leg die drei bis fünf Seiten, die Leute jeden Tag öffnen, in eine Bottom Tab Bar, wo der Daumen ohnehin liegt, und lass die komplette Navigation im Drawer hinter einem Menü-Button oder einem Mehr-Tab.

---

Quelle: https://wingo-ui.com/de/blog/shadcn-sidebar-mobile
