Zum Inhalt springen
    WingoUI
    KomponentenVorlagenDokuPreiseKI-AgentenBlogChangelog
    1. Startseite
    2. Blog
    3. Praxis: Screens und Apps
    4. Nextjs Admin Dashboard: 5 Screens, fit fürs Smartphone
    Read in English
    1. Blog
    2. Praxis: Screens und Apps
    3. Nextjs Admin Dashboard: 5 Screens, fit fürs Smartphone
    Praxis: Screens und Apps
    Praxis: Screens und Apps

    Nextjs Admin Dashboard: 5 Screens, fit fürs Smartphone

    SR
    Serban Rusu · Gründer von Wingo UI
    9. Okt. 2026 · 21 Min. Lesezeit

    Auf dieser Seite

    0%
    1. Was braucht ein Next.js Admin Dashboard?
    2. Wie strukturierst du den App Router für ein Admin Dashboard?
    3. Wie baust du die App Shell für ein Next.js Admin Dashboard?
    4. Wie baust du die Dashboard-Startseite mit KPIs?
      1. Wann nimmst du stattdessen das KPI Dashboard?
      2. Kannst du KPI-Karten aus einer Server Component rendern?
    5. Wie baust du Listenseiten mit einer serverseitigen Datentabelle?
    6. Wie baust du eine Settings-Seite, die keine Eingaben verliert?
    7. Wie baust du die Seite für Team und Rollen?
    8. Wie gehst du im Admin-Bereich mit Lade- und Fehlerzuständen um?
    9. Wie machst du jeden Admin-Screen auf dem Smartphone nutzbar?
    10. Solltest du mit einem Next.js Admin Template starten oder selbst bauen?
    11. Welche Tutorials bauen die anderen Screens?
    12. Was solltest du zuerst bauen?
    13. Häufige Fragen
      1. Wie strukturiere ich ein Next.js-App-Router-Projekt für ein Admin Dashboard?
      2. Gibt es ein kostenloses shadcn Admin Dashboard für Next.js?
      3. Soll eine Admin-Datentabelle auf dem Server oder im Browser filtern?
      4. Wohin gehören Auth-Checks in einem Next.js Admin Dashboard?
      5. Wie mache ich ein Admin Dashboard auf dem Smartphone nutzbar?
      6. Welche Admin-Komponenten von Wingo UI sind kostenlos?

    Kurz gesagt

    Bau ein Next.js Admin Dashboard als fünf Screens in einer Route Group des App Routers: ein Layout mit App Shell, eine Startseite mit KPIs, Listenseiten mit einer serverseitigen Datentabelle, deren Zustand in der URL liegt, eine Settings-Seite und eine Team-Seite mit Rollen. Die Seiten laden ihre Daten über einen Data Access Layer, der nur auf dem Server läuft, und jede Änderung geht durch eine Server Action, die die Session selbst prüft. Auf dem Smartphone wird die Sidebar zum Drawer mit Bottom Tab Bar, Tabellenzeilen werden zu Karten und Menüs öffnen sich als Bottom Sheets.

    Veröffentlicht am 9. Okt. 2026

    Die meisten Nextjs Admin Dashboard Tutorials hören bei einer Sidebar, vier Stat Cards, einem Chart und einer Tabelle auf, die auf dem Smartphone seitlich scrollt. Ein Backoffice braucht mehr: Listenseiten, die auf dem Server filtern, eine Settings-Seite, die das Getippte behält, wenn das Speichern scheitert, eine Team-Seite mit Rollen, Lade- und Fehlerzustände auf jeder Route und ein Layout, das auch funktioniert, wenn die Person, die eine Rechnung freigibt, gerade am Smartphone sitzt. Dieses Tutorial baut all das Screen für Screen mit Code, auf dem Next.js App Router mit React 19 und Tailwind CSS v4. Wir bauen Wingo UI, und die Beispiele nutzen unsere Blöcke. Wir sind also nicht neutral; die Muster für den App Router funktionieren mit jeder Komponentenbibliothek, und der letzte Abschnitt vergleicht die kostenlosen Starter.

    Was braucht ein Next.js Admin Dashboard?

    Fünf Screens und zwei Zustände: eine Shell um eine Startseite, Listenseiten, eine Settings-Seite und eine Team-Seite, jede mit einem Lade- und einem Fehlerzustand. Die React Admin Dashboard Komponenten, die wir für jeden Screen nutzen, und wie sie sich auf dem Smartphone verhalten:

    ScreenAufgabeGebaut mitAuf dem Smartphone
    ShellNavigation, Suche, das KontoApp Shell mit SidebarDie Sidebar wandert in einen Drawer, eine Tab Bar belegt den unteren Rand
    StartseiteWas sich geändert hat, was Aufmerksamkeit brauchtAdmin Dashboard Page oder KPI DashboardKPIs zu zweit pro Reihe, Panels in einer Spalte
    ListenDatensätze finden, filtern und bearbeitenData Table im Server-ModusZeilen werden zu Karten, Menüs zu Bottom Sheets
    SettingsProfil, Konto, Abrechnung, SicherheitSettings PageEine Liste, die jeden Bereich hereinschiebt, wie die iOS-Einstellungen
    TeamMitglieder, Rollen, Audit-LogUsers Admin PageMitgliederkarten, in der Matrix eine Rolle nach der anderen
    Laden und FehlerJede Routeloading.tsx, error.tsx und das loading-Prop jedes BlocksSkeletons im Smartphone-Layout

    Wie strukturierst du den App Router für ein Admin Dashboard?

    Leg alle Admin-Routen in eine Route Group mit eigenem Layout. Ein Ordner in Klammern taucht in der URL nicht auf (opens in a new tab), also liefert app/(admin)/clients/page.tsx die Route /clients aus, während die layout.tsx der Gruppe sie in die Shell packt. Die Anmeldung liegt in einer eigenen Gruppe und sieht die Sidebar nie.

    text
    app/
    layout.tsx Root: html, body, <Providers>
    providers.tsx LinkProvider (next/link) und der Toaster
    (auth)/
    sign-in/page.tsx keine Shell
    (admin)/
    layout.tsx die Shell: AppShell, Sidebar, Topbar
    nav.tsx Sidebar-Gruppen und Smartphone-Tabs
    error.tsx die Error Boundary des Admins
    page.tsx /: die Startseite mit KPIs
    dashboard-home.tsx
    actions.ts Bestellaktionen für die Startseite
    clients/
    page.tsx liest die URL, lädt eine Seite
    clients-table.tsx
    query.ts
    loading.tsx
    [id]/page.tsx ein Kunde
    settings/
    page.tsx
    settings-view.tsx
    actions.ts
    team/
    page.tsx
    team-view.tsx
    actions.ts
    server/
    dal.ts "server-only": verifySession, getUser, requireAdmin

    Drei Regeln halten das zusammen:

    1. Seiten laden, Views rendern. Jede page.tsx ist eine Server Component, die Daten lädt. Die Blöcke sind Client Components, also leben Funktions-Props (ein Save-Handler, ein Router-Aufruf) in einer kleinen *-view.tsx neben der Seite.
    2. Auth gehört in den Data Access Layer. Layouts rendern bei der Navigation nicht neu (opens in a new tab), ein Check im Layout verpasst also Routenwechsel. Ruf verifySession() aus einem server-only-Modul in jeder Seite und jeder Server Action auf; der Guide zur Datensicherheit (opens in a new tab) rät, jede Server Action so zu behandeln, als wäre sie per direktem POST-Request erreichbar.
    3. Die Ansicht steckt in der URL. Zeitraum, Filter, Seite, offener Tab: Wenn es ändert, was der Screen zeigt, gehört es in die Search Params, damit ein Link den Screen reproduziert.

    Wie baust du die App Shell für ein Next.js Admin Dashboard?

    Render eine einzige AppShell im Layout der Gruppe, mit einer Sidebar und einer Topbar, und lies das Cookie der Sidebar auf dem Server, damit der erste Paint stimmt. Die Shell zeigt die Sidebar am Desktop neben der Seite und auf dem Smartphone einen Drawer plus Bottom Tab Bar. Sie schaltet per CSS um, das Server-HTML ist also schon richtig.

    Zuerst die Router-Anbindung: Die Navigationslinks von Wingo UI rendern über LinkProvider, also macht ein einziger Client-Provider aus allen next/link:

    tsx
    // app/providers.tsx
    "use client";
    import type { ReactNode } from "react";
    import Link from "next/link";
    import { usePathname, useRouter } from "next/navigation";
    import { Toaster } from "@/components/ui/toast";
    import { LinkProvider } from "@/lib/navigation";
    export function Providers({ children }: { children: ReactNode }) {
    const router = useRouter();
    return (
    <LinkProvider component={Link} currentPath={usePathname()} navigate={router.push}>
    {children}
    {/* auf dem Smartphone liegt unten eine Tab Bar, also kommen Toasts von oben */}
    <Toaster mobilePosition="top" />
    </LinkProvider>
    );
    }

    Sidebar und Tab Bar nutzen denselben Typ NavItem, also speist eine Datei beide:

    tsx
    // app/(admin)/nav.tsx
    import { House, Settings, ShieldCheck, Users } from "lucide-react";
    import type { SidebarNavGroup } from "@/components/ui/sidebar";
    import type { NavItem } from "@/lib/navigation";
    export const nav: SidebarNavGroup[] = [
    { items: [{ label: "Start", href: "/", icon: <House /> }] },
    { label: "Vertrieb", items: [{ label: "Kunden", href: "/clients", icon: <Users />, badge: 3 }] },
    {
    label: "Workspace",
    items: [
    { label: "Team", href: "/team", icon: <ShieldCheck /> },
    { label: "Einstellungen", href: "/settings", icon: <Settings /> },
    ],
    },
    ];
    export const tabs: NavItem[] = [
    { label: "Start", href: "/", icon: <House />, match: "exact" },
    { label: "Kunden", href: "/clients", icon: <Users /> },
    { label: "Team", href: "/team", icon: <ShieldCheck /> },
    { label: "Einstellungen", href: "/settings", icon: <Settings /> },
    ];

    Und das Layout, eine Server Component:

    tsx
    // app/(admin)/layout.tsx
    import type { ReactNode } from "react";
    import { cookies } from "next/headers";
    import { AppShell } from "@/components/ui/app-shell";
    import { Sidebar } from "@/components/ui/sidebar";
    import { Topbar } from "@/components/ui/topbar";
    import { getUser } from "@/server/dal";
    import { nav, tabs } from "./nav";
    export default async function AdminLayout({ children }: { children: ReactNode }) {
    const [store, user] = await Promise.all([cookies(), getUser()]);
    // die Shell schreibt `${persistKey}-collapsed` (standardmäßig "app-shell") als "true" oder "false"
    const collapsed = store.get("app-shell-collapsed")?.value === "true";
    return (
    <AppShell
    defaultCollapsed={collapsed}
    mobileBreakpoint="lg"
    sidebar={<Sidebar groups={nav} filter user={{ name: user.name, email: user.email, avatar: user.avatarUrl }} />}
    topbar={<Topbar title="Northwind" />}
    tabs={tabs}
    >
    {children}
    </AppShell>
    );
    }

    Mod+B (Cmd auf dem Mac, sonst Ctrl) klappt die Sidebar zu einer 64px breiten Icon-Leiste ein und speichert das im Cookie app-shell-collapsed. Ohne das Auslesen auf dem Server sieht jemand, der sie eingeklappt hat, sie bei jedem ersten Paint ausgeklappt, bis sie nach der Hydration auf die Leiste springt. Nimm auf dem Server den Cookie-Namen als Literal: Der Helper persistedCookieName liegt in einer Hook-Datei, die reine Client-Hooks von React importiert, und die kann ein Server-Layout nicht importieren. Unterhalb des Breakpoints wird die Sidebar zum Drawer, der sich bei Navigation, Escape, einem Tipp daneben oder einer Zurück-Wischgeste schließt, und der Fokus kehrt zum Menü-Button zurück. Ein Link „Zum Inhalt springen“ ist der erste Tab-Stopp.

    mobileBreakpoint="lg" (1024px) setzen wir mit Absicht. Bei md (768px) behält ein 820px breites Tablet im Hochformat eine 256px breite Sidebar und 564px für die Seite, zu wenig für eine Tabelle. Bei lg bekommt es das Smartphone-Layout und die volle Breite.

    Halt die Awaits in diesem Layout schnell: Ein langsames await hier hält jede Seite dahinter auf (opens in a new tab). Ein Cookie lesen und eine gecachte Session-Abfrage sind in Ordnung, eine Dashboard-Query nicht.

    Zum Vergleich: Die Sidebar von shadcn/ui (Stand Oktober 2026) speichert, ob sie offen ist, (opens in a new tab) sieben Tage lang in einem Cookie sidebar_state und rendert ein Sheet, wenn ihr Hook useIsMobile ein Smartphone meldet. Eine Bottom Tab Bar hat sie nicht, auf dem Smartphone ist also jedes Ziel zwei Tipps entfernt.

    Wer das komplette Backoffice-Layout in einem Block will: Die Admin Shell ergänzt einen Workspace-Umschalter, eine globale Suche auf /, Benachrichtigungen, ein Command Menu auf Mod+K und eine Tab Bar mit fünf Tabs, deren Tab „Mehr“ den Drawer öffnet. Unser Guide zur React Command Palette behandelt die Shortcuts dieses Menüs, verschachtelte Seiten und das Sheet auf dem Smartphone.

    Wie baust du die Dashboard-Startseite mit KPIs?

    Lade die Zahlen in der Seite für den Zeitraum aus der URL und gib sie als Props an einen Dashboard-Block. Die Admin Dashboard Page ist die Startseite eines Geschäfts mit Bestellungen (vier KPIs, Umsatz im Vergleich zum Vorzeitraum, letzte Bestellungen, was Aufmerksamkeit braucht, Team-Aktivität); das KPI Dashboard passt zu einer B2B- oder Analytics-App.

    Die Seite liest den Zeitraum, nimmt standardmäßig die letzten 30 Tage und lädt:

    tsx
    // app/(admin)/page.tsx
    import { isValid, parseISO, startOfDay, subDays } from "date-fns";
    import { getDashboard } from "@/server/dashboard";
    import { getUser } from "@/server/dal";
    import { DashboardHome } from "./dashboard-home";
    function readPeriod(from?: string, to?: string) {
    const today = startOfDay(new Date());
    const end = to ? parseISO(to) : today;
    const start = from ? parseISO(from) : subDays(end, 29);
    return isValid(start) && isValid(end) ? { from: start, to: end } : { from: subDays(today, 29), to: today };
    }
    export default async function HomePage({ searchParams }: { searchParams: Promise<{ from?: string; to?: string }> }) {
    const { from, to } = await searchParams;
    const period = readPeriod(from, to);
    // { kpis, revenue, orders, tasks, activity, topProducts } für den Zeitraum
    const [user, data] = await Promise.all([getUser(), getDashboard(period)]);
    return <DashboardHome firstName={user.firstName} period={period} data={data} />;
    }

    Die View schreibt einen neuen Zeitraum zurück in die URL und schickt Bestellaktionen an Server Actions:

    tsx
    // app/(admin)/dashboard-home.tsx
    "use client";
    import { useRouter } from "next/navigation";
    import { format } from "date-fns";
    import { AdminDashboardPage } from "@/components/blocks/admin-dashboard-page";
    import type { DashboardData } from "@/components/blocks/admin-dashboard-page-data";
    import type { DateRange } from "@/components/ui/date-range-picker";
    import { runOrderAction } from "./actions";
    type Props = { firstName: string; period: DateRange; data: DashboardData };
    export function DashboardHome({ firstName, period, data }: Props) {
    const router = useRouter();
    return (
    <AdminDashboardPage
    {...data}
    userName={firstName}
    defaultPeriod={period}
    onPeriodChange={({ from, to }) => {
    if (!from || !to) return;
    const params = new URLSearchParams({ from: format(from, "yyyy-MM-dd"), to: format(to, "yyyy-MM-dd") });
    router.replace(`/?${params}`, { scroll: false });
    }}
    onOrderAction={async (action, order) => {
    const { error } = await runOrderAction(action, order.id);
    if (error) throw new Error(error);
    }}
    />
    );
    }

    Der Zeitraum ist mit Absicht uncontrolled (defaultPeriod): Das Datumsfeld aktualisiert sich in dem Moment, in dem jemand einen Zeitraum übernimmt, und die Panels folgen, sobald der Server antwortet. onOrderAction ist optimistisch: Der Status ändert sich sofort, die Zeile zeigt einen Spinner, solange das Promise läuft, und eine Rejection rollt die Änderung mit einem Fehler-Toast zurück. Deshalb wirft der Handler bei einem Fehler. Ein Haken: Nach einer Stornierung stellt das Undo im Toast die Zeile nur auf dem Bildschirm wieder her und ruft nichts von deinem Code auf, also zeigt der nächste Server-Render die Bestellung wieder als storniert. Schreib labels.cancelDescription um, damit Nutzer wissen, dass eine Stornierung endgültig ist; der Standardtext verspricht eine E-Mail und eine Rückerstattung, und das kann nur dein Server halten.

    Wähl im Datumsfeld einen anderen Zeitraum und klick dann unter Recent orders auf eine Bestellung, um ihr Sheet zu öffnen. Dieser Rahmen ist schmaler als 1024px, du siehst also das Smartphone-Layout: Die Sidebar liegt hinter dem Menü-Button, und die Tab Bar belegt den unteren Rand
    $ npx wingo-ui@latest add admin-dashboard-page
    ProDoku: Admin Dashboard Page

    Auf dem Smartphone stehen die KPIs zwei mal zwei, die letzten Bestellungen werden zu Karten mit „Mehr laden“, und Datumsfeld und Bestelldetails öffnen sich als Bottom Sheets, die du zum Schließen nach unten ziehst.

    Wann nimmst du stattdessen das KPI Dashboard?

    Wenn es auf der Startseite mehr um Analytics als um Bestellungen geht: Umsatz über die Zeit, Verkäufe nach Quelle, ein Conversion-Funnel, Top-Verkäufer und Rechnungen. Es nimmt ein einziges data-Objekt und blendet ein Widget aus, dessen Daten fehlen, passt also auch zu einer Firma ohne Funnel. Es hat die Controls, die eine Analytics-Seite braucht: onRefresh (ein Promise zeigt am Button einen Spinner und kündigt das Update an), onExport mit CSV und PDF, eine widgets-Liste, um Panels auszuwählen und zu ordnen, und widgetErrors mit onWidgetRetry, um einen Fehler und einen Button „Erneut versuchen“ genau in dem Widget zu zeigen, dessen Query gescheitert ist. Der Rest der Seite funktioniert weiter.

    Sein Grid nutzt Container Queries: eine Spalte, zwei ab 672px und drei ab 1152px der eigenen Breite. In einer Shell zählt das, denn ein 1280px breites Fenster mit 256px Sidebar lässt etwa 1000px übrig, und Viewport-Breakpoints würden das Grid für Platz auslegen, den es nicht hat. Die Parts DashboardWidget und KpiDashboardGrid sind für eigene Panels exportiert.

    Kannst du KPI-Karten aus einer Server Component rendern?

    Ja, solange jedes Prop Daten ist. Die Stat Card nimmt Zahlen, Strings und Icons, eine Server Component kann also direkt eine Reihe rendern:

    tsx
    // app/(admin)/reports/page.tsx
    import { FileText, Timer, UserPlus, Wallet } from "lucide-react";
    import { StatCard, StatGroup } from "@/components/ui/stat-card";
    import { getKpis } from "@/server/reports";
    export default async function ReportsPage() {
    const k = await getKpis();
    return (
    <StatGroup variant="joined">
    <StatCard label="Umsatz" icon={<Wallet />} value={k.revenue} format="currency" delta={k.revenueChange} deltaLabel="ggü. Vormonat" sparkline={k.dailyRevenue} goal={k.revenueGoal} locale="en-US" />
    <StatCard label="Ausgestellte Rechnungen" icon={<FileText />} value={k.invoices} delta={k.invoicesChange} deltaFormat="number" locale="en-US" />
    <StatCard label="Neue Kunden" icon={<UserPlus />} value={k.newClients} delta={k.newClientsChange} locale="en-US" />
    <StatCard label="Zeit bis zur Zahlung" icon={<Timer />} value={k.secondsToPay} format="duration" delta={k.timeToPayChange} invertTrend locale="en-US" />
    </StatGroup>
    );
    }

    Das Server-HTML enthält die endgültigen Zahlen, das Hochzählen läuft auf dem Client, wenn eine Karte ins Bild scrollt, und Screenreader hören einen Satz mit dem endgültigen Wert und der Veränderung. invertTrend färbt einen Rückgang bei der Zeit bis zur Zahlung grün. Zwei Grenzen: Ein Funktions-Prop wie formatDelta braucht eine Client-Datei, und href rendert einen einfachen Anchor (in Next.js ein kompletter Seitenaufruf), verlinkte Kacheln brauchen also einen Client-Wrapper, dessen onClick erst event.preventDefault() und dann router.push aufruft. Für die Charts rund um diese Kacheln behandelt unser Guide zu React Charts mit Tailwind Area-, Bar-, Line- und Donut-Charts, und der Guide zur React Sparkline kleine Trends in Kacheln und Tabellenzellen.

    Wie baust du Listenseiten mit einer serverseitigen Datentabelle?

    Lass die Data Table im Server-Modus laufen, halt ihre Query in der URL und lade pro Request nur eine Seite mit Zeilen. Mit mode="server" sortiert, filtert und paginiert die Tabelle nicht selbst: data ist die aktuelle Seite, rowCount die Gesamtzahl, und jede Änderung kommt in onQueryChange als ein einziges Objekt an (page, pageSize, sorting, globalFilter, columnFilters). Ein Modul übersetzt es in die Search Params und zurück:

    ts
    // app/(admin)/clients/query.ts
    import type { DataTableQuery } from "@/components/ui/data-table";
    type Params = Record<string, string | string[] | undefined>;
    const one = (value: string | string[] | undefined) => (Array.isArray(value) ? value[0] : value) ?? "";
    export function readQuery(params: Params): DataTableQuery {
    const [sortId, dir] = one(params.sort).split(".");
    const status = one(params.status);
    const size = Number(one(params.size));
    return {
    page: Math.max(1, Number(one(params.page)) || 1),
    pageSize: [10, 25, 50, 100].includes(size) ? size : 25,
    sorting: sortId ? [{ id: sortId, desc: dir === "desc" }] : [],
    globalFilter: one(params.q),
    columnFilters: status ? [{ id: "status", value: status.split(",") }] : [],
    };
    }
    export function writeQuery(query: DataTableQuery): string {
    const params = new URLSearchParams();
    if (query.page > 1) params.set("page", String(query.page));
    if (query.pageSize !== 25) params.set("size", String(query.pageSize));
    if (query.globalFilter) params.set("q", query.globalFilter);
    const sort = query.sorting[0];
    if (sort) params.set("sort", `${sort.id}.${sort.desc ? "desc" : "asc"}`);
    const status = query.columnFilters.find((filter) => filter.id === "status")?.value;
    if (Array.isArray(status) && status.length) params.set("status", status.join(","));
    return params.toString();
    }

    Die Seite parst die URL und fragt eine Seite ab:

    tsx
    // app/(admin)/clients/page.tsx
    import { listClients } from "@/server/clients";
    import { ClientsTable } from "./clients-table";
    import { readQuery } from "./query";
    type Search = Promise<Record<string, string | string[] | undefined>>;
    export default async function ClientsPage({ searchParams }: { searchParams: Search }) {
    const query = readQuery(await searchParams);
    // prüft die Session und gleicht Sortier-IDs und Filterwerte mit einer Whitelist ab, bevor SQL entsteht
    const { rows, total } = await listClients(query);
    return <ClientsTable rows={rows} total={total} initial={query} />;
    }

    Die View startet mit der URL und schreibt Änderungen in einer Transition zurück, also steuert isPending den Aktualisierungsbalken:

    tsx
    // app/(admin)/clients/clients-table.tsx
    "use client";
    import { useTransition } from "react";
    import { useRouter } from "next/navigation";
    import { DataTable, createColumns, type DataTableQuery } from "@/components/ui/data-table";
    import { writeQuery } from "./query";
    export type ClientRow = { id: string; name: string; city: string; status: string; balance: number; lastInvoice: string | null };
    const STATUSES = ["Active", "Overdue", "Prospect"];
    const col = createColumns<ClientRow>();
    const columns = col([
    col.accessor("name", { header: "Kunde", mobile: "title" }),
    col.accessor("city", { header: "Stadt", mobile: "subtitle" }),
    col.accessor("status", {
    header: "Status",
    type: "badge",
    badge: { Active: { tone: "success" }, Overdue: { tone: "danger" }, Prospect: { tone: "info" } },
    filter: "multi-select",
    // der Server-Modus sieht nur eine Seite, also listest du jede Option selbst auf
    filterOptions: STATUSES.map((status) => ({ value: status, label: status })),
    mobile: "meta",
    }),
    col.accessor("balance", { header: "Saldo", type: "currency" }),
    col.accessor("lastInvoice", { header: "Letzte Rechnung", type: "relative" }),
    ]);
    type Props = { rows: ClientRow[]; total: number; initial: DataTableQuery; loading?: boolean };
    export function ClientsTable({ rows, total, initial, loading }: Props) {
    const router = useRouter();
    const [pending, startTransition] = useTransition();
    return (
    <DataTable
    data={rows}
    columns={columns}
    mode="server"
    rowCount={total}
    defaultPage={initial.page}
    defaultPageSize={initial.pageSize}
    defaultSorting={initial.sorting}
    defaultGlobalFilter={initial.globalFilter}
    defaultColumnFilters={initial.columnFilters}
    onQueryChange={(query) => {
    const search = writeQuery(query);
    startTransition(() => router.replace(search ? `/clients?${search}` : "/clients", { scroll: false }));
    }}
    refreshing={pending}
    loading={loading}
    quickFilters={[{ id: "overdue", label: "Überfällig", columnFilter: { id: "status", value: ["Overdue"] } }]}
    onRowClick={(row) => router.push(`/clients/${row.id}`)}
    caption="Kunden"
    locale="en-US"
    />
    );
    }

    Details, die Debugging-Zeit sparen: onQueryChange feuert beim ersten Render nicht (diese Seite hat der Server schon geschickt), die Suche kommt mit Debounce an (300ms), und die Seite springt auf 1 zurück, wenn sich Suche, ein Filter oder die Sortierung ändert. Die Tabelle ist uncontrolled (default*-Props), also hinkt das Suchfeld nie einem Server-Roundtrip hinterher. Der Preis dafür: Der Zurück-Button ändert die Daten, während die Toolbar die letzte Query behält. Wenn deine Nutzer ständig den Zurück-Button nutzen, steuer page, sorting, globalFilter und columnFilters stattdessen controlled aus der URL.

    Such, sortier oder klick auf den Chip Overdue: Im Server-Modus übergibt die Tabelle deinem Code ein einziges Query-Objekt, das unter dem Pager ausgegeben wird. Unter 768px ist jede Zeile eine Karte, die du per Long Press auswählst
    $ npx wingo-ui@latest add data-table
    ProDoku: Data Table

    Auf dem Smartphone werden die Spalten zu Karten: mobile: "title" ist die Überschrift, subtitle die Zeile darunter, meta das Badge in der Ecke. Sortierung, Filter und das Zeilenmenü öffnen sich als Bottom Sheets, und mit selectionMode startet ein Long Press die Auswahl. Der Server-Modus behält auf dem Smartphone einen kompakten Pager; der Client-Modus nutzt „Mehr laden“.

    Der Server-Modus ist nicht immer die richtige Wahl. Wenn eine einzige Anfrage alle Zeilen laden kann, die ein Nutzer sehen darf, filtert der Client-Modus sofort und braucht keine Query-API. Wir wechseln ab ein paar tausend Zeilen in den Server-Modus, oder wenn Berechtigungen bestimmen, welche Zeilen für einen Nutzer existieren. Wenn du die Tabelle lieber selbst auf TanStack Table baust, mit derselben Server-Paginierung, folg unserem Tutorial zur Datentabelle mit TanStack Table v9.

    Wie baust du eine Settings-Seite, die keine Eingaben verliert?

    Lass einen Block die Entwürfe verwalten und speicher über Server Actions, die ihre Fehler zurückgeben. Die Settings Page hat sechs Bereiche (Profil, Konto, Benachrichtigungen, Abrechnung, Sicherheit, Danger Zone). Eine Textänderung schiebt eine Save Bar mit Verwerfen und Speichern (oder Mod+S) hoch, Switches speichern selbst, ein gescheitertes Speichern behält den Entwurf und zeigt die Meldung, und wer einen Bereich mit ungespeicherten Änderungen verlässt, wird vorher gefragt.

    Next.js empfiehlt, erwartbare Fehler in Server Functions als Rückgabewerte zu modellieren (opens in a new tab), und in Produktion bekommt der Client die Originalmeldung eines Serverfehlers nicht zu sehen. Ein geworfenes „Diese Workspace-Adresse ist vergeben“ käme also als generischer Fehler an:

    ts
    // app/(admin)/settings/actions.ts
    "use server";
    import { verifySession } from "@/server/dal";
    import { saveSettings } from "@/server/settings";
    export type ActionResult = { error?: string };
    export async function saveSection(section: string, values: unknown): Promise<ActionResult> {
    const { userId } = await verifySession();
    // validiert mit zod und gibt { error } für alles zurück, was der Nutzer korrigieren kann
    return saveSettings(userId, section, values);
    }
    // savePreference(preference, value) hat dieselbe Form. verifyEmailCode(email, code),
    // verifyTwoFactorCode(code) und checkWorkspaceAddress(slug) prüfen ebenfalls die Session
    // und liefern einen Boolean (true: der Code stimmt, die Adresse ist frei)

    Die View macht aus einem zurückgegebenen Fehler die Rejection, auf die der Block hört, und ersetzt die Demo-Prüfungen des Blocks durch echte:

    tsx
    // app/(admin)/settings/settings-view.tsx
    "use client";
    import { SettingsPage, type SettingsPageProps } from "@/components/blocks/settings-page";
    import {
    checkWorkspaceAddress,
    savePreference,
    saveSection,
    verifyEmailCode,
    verifyTwoFactorCode,
    type ActionResult,
    } from "./actions";
    type Props = Pick<
    SettingsPageProps,
    | "profile"
    | "account"
    | "notifications"
    | "billing"
    | "plans"
    | "sessions"
    | "passwordChangedAt"
    | "defaultTwoFactorEnabled"
    | "twoFactorSecret"
    | "twoFactorUri"
    | "backupCodes"
    >;
    const LABELS = { codeHint: "Gib den 6-stelligen Code ein." };
    async function check(result: Promise<ActionResult>) {
    const { error } = await result;
    if (error) throw new Error(error);
    }
    export function SettingsView(props: Props) {
    return (
    <SettingsPage
    {...props}
    onSave={(section, values) => check(saveSection(section, values))}
    onPreferenceChange={(preference, value) => check(savePreference(preference, value))}
    verifyEmailCode={verifyEmailCode}
    verifyTwoFactorCode={verifyTwoFactorCode}
    checkWorkspaceAddress={checkWorkspaceAddress}
    labels={LABELS}
    />
    );
    }

    Gib jedes Daten-Prop aus der Seite mit: Alles, was du weglässt, fällt auf die Beispieldaten des Blocks zurück. Die Sicherheits-Defaults sind mit Absicht Demo-Werte. Ohne verifyEmailCode und verifyTwoFactorCode geht der Code 123456 durch; ohne twoFactorSecret, twoFactorUri und backupCodes zeigt der Setup-Dialog Beispielwerte; und das Standard-Label codeHint sagt sinngemäß, dass der Code in dieser Demo 123456 ist. Erzeug Secret und Backup-Codes auf dem Server für den angemeldeten Nutzer.

    Die Dialoge nehmen Handler mit demselben Promise-Vertrag (onEmailChange, onPasswordChange, onTwoFactorChange, onSessionRevoke, onPlanChange, onDeleteAccount). Ein Detail zur Benennung: Die Bereichs-IDs sind intern (profil, cont, notificari, facturare, securitate, pericol). Bilde sie also auf deine eigenen Segmente ab, wenn jeder Bereich über onSectionChange eine eigene URL bekommt.

    Öffne Profile, ändere den vollen Namen und drück Save. Diese Demo lässt das erste Speichern absichtlich scheitern: Die Leiste zeigt den Fehler, deine Änderung bleibt, und das nächste Save geht durch. In einem so schmalen Rahmen sind die Bereiche eine Liste, die jeden einzelnen hereinschiebt
    $ npx wingo-ui@latest add settings-page
    ProDoku: Settings Page

    Ab 768px stehen die Bereiche in einer Spalte links (oder als Tabs mit nav="tabs"). Auf dem Smartphone schiebt sich jeder Bereich mit Zurück-Button und Zurück-Wischgeste herein, Dialoge öffnen sich als Bottom Sheets, und die Save Bar ist über der Tab Bar fixiert. Der Block validiert jedes Formular, bevor er onSave aufruft: Ungültige Felder werden rot, und das erste bekommt den Fokus. Dieses Muster behandelt unser Guide zu React-Formularkomponenten Feld für Feld.

    Wie baust du die Seite für Team und Rollen?

    Behandle jedes Control darauf als Anfrage, die der Server ablehnen kann. Die Users Admin Page hat drei Tabs: Mitglieder (das Team und offene Einladungen, abgeglichen mit deinen Seats, ein Rollen-Picker pro Zeile), Rollen und Berechtigungen (eine Matrix mit Save Bar) und Audit-Log (eine filterbare Historie mit CSV-Export). Der Server entscheidet, wer verwalten darf, und setzt die Regeln durch, die die Oberfläche zeigt:

    ts
    // app/(admin)/team/actions.ts
    "use server";
    import { requireAdmin } from "@/server/dal";
    import { changeMemberRole } from "@/server/team";
    export async function changeRole(memberId: string, role: string): Promise<{ error?: string }> {
    const admin = await requireAdmin();
    // verweigert die Herabstufung des letzten Administrators, dieselbe Regel, die der Picker zeigt
    return changeMemberRole(admin.workspaceId, memberId, role);
    }
    // invite, removeMember und savePermissions haben dieselbe Form
    tsx
    // app/(admin)/team/team-view.tsx
    "use client";
    import { UsersAdminPage, type UsersAdminPageProps } from "@/components/blocks/users-admin-page";
    import { changeRole, invite, removeMember, savePermissions } from "./actions";
    type Props = Pick<
    UsersAdminPageProps,
    "companyName" | "members" | "invites" | "roles" | "permissionRoles" | "permissionGroups" | "defaultPermissions" | "auditEvents" | "seats" | "canManage"
    >;
    async function check(result: Promise<{ error?: string }>) {
    const { error } = await result;
    if (error) throw new Error(error);
    }
    export function TeamView(props: Props) {
    return (
    <UsersAdminPage
    {...props}
    onInvite={(input) => check(invite(input))}
    onRoleChange={(member, role) => check(changeRole(member.id, role))}
    onRemove={(member) => check(removeMember(member.id))}
    onPermissionsSave={(value) => check(savePermissions(value))}
    />
    );
    }

    Die Seite berechnet canManage aus der Session und gibt es mit den Daten weiter. Mit canManage={false} schaltet der Block auf Nur-Lesen und sagt, warum. Das ist besser, als die Seite vor Leuten zu verstecken, die nur sehen müssen, wer Zugriff hat.

    Die Oberfläche fängt die offensichtlichen Fehler ab: Der letzte Administrator lässt sich nicht herabstufen, niemand entfernt sich selbst oder den Inhaber, und Einladungen belegen einen Seat. Diese Schutzregeln sind für die Person, die klickt; deine Actions setzen sie noch einmal durch, denn eine Server Action kann nicht erkennen, welcher Button die Anfrage geschickt hat. Genauso setzt der Block die Änderungen dieser Seite als Feedback oben ins Audit-Log, die echten Audit-Einträge sind aber die, die dein Server in derselben Transaktion schreibt.

    Auf dem Smartphone werden Mitglieder zu Karten mit dem Rollen-Picker darin, der Einladungsdialog ist ein Bottom Sheet, und die Matrix zeigt mit Chips eine Rolle nach der anderen, weil fünf Rollenspalten nicht in 390px passen. Die Teile gibt es auch einzeln: Members Table, Permissions Matrix und Audit Log.

    Wie gehst du im Admin-Bereich mit Lade- und Fehlerzuständen um?

    Gib jeder Route eine loading.tsx, die dieselbe Komponente mit ihrem loading-Prop rendert, und der Gruppe eine error.tsx. loading.js (opens in a new tab) zeigt seinen Fallback bei der Navigation sofort, während die Shell interaktiv bleibt, und ein Skeleton im echten Layout bedeutet, dass nichts springt, wenn die Daten ankommen:

    tsx
    // app/(admin)/clients/loading.tsx
    import { ClientsTable } from "./clients-table";
    import { readQuery } from "./query";
    export default function Loading() {
    return <ClientsTable rows={[]} total={0} initial={readQuery({})} loading />;
    }

    Die Data Table zeichnet Skeleton-Zeilen in ihrem Spaltenlayout (auf dem Smartphone als Karten), und die Dashboard-, Settings- und Team-Blöcke haben dasselbe Prop. Eine Route ohne eigene loading.tsx fällt auf die des nächsthöheren Segments zurück, ein einziges Skeleton auf Gruppenebene würde also vor jeder Seite dasselbe Layout aufblitzen lassen.

    Für Fehler hat Next.js 16.3 das Prop retry von error.js (opens in a new tab) stabil gemacht; es lädt das Segment neu und rendert es erneut:

    tsx
    // app/(admin)/error.tsx
    "use client";
    import { Button } from "@/components/ui/button";
    export default function AdminError({ error, retry }: { error: Error & { digest?: string }; retry: () => void }) {
    return (
    <div role="alert" className="mx-auto flex max-w-md flex-col items-center gap-3 px-4 py-16 text-center">
    <h2 className="text-lg font-semibold">Diese Seite konnte nicht geladen werden</h2>
    <p className="text-sm text-muted-foreground">Wenn der Fehler bleibt, schick uns diese Referenz: {error.digest ?? "keine"}</p>
    <Button onClick={retry}>Erneut versuchen</Button>
    </div>
    );
    }

    Leere Zustände gehören in die Komponenten: emptyState der Data Table für eine Liste ohne Zeilen und noResultsState, wenn Filter nichts finden. So werden „Noch keine Kunden“ und „Keine Treffer“ nie verwechselt.

    Wie machst du jeden Admin-Screen auf dem Smartphone nutzbar?

    Pass das Verhalten an, nicht nur das Layout, und prüf jeden Screen bei 390px auf einem echten Smartphone. Leute öffnen ein Backoffice auf dem Smartphone, um eine Ausgabe freizugeben, eine Bestellung zu prüfen oder einem Kunden zu antworten. Die Checkliste, an der wir jeden Admin-Screen messen:

    • Navigation: alles in einem Drawer hinter dem Menü-Button, dazu eine Tab Bar für die drei bis fünf täglichen Ziele; Tablets im Hochformat bekommen das Smartphone-Layout.
    • Eine Leiste am unteren Rand: Die Tab Bar hält Abstand zum Home-Indikator und veröffentlicht ihre Höhe als --bottom-bar-offset, und die Save Bars von Settings und Team lesen sie, um über der Tab Bar zu sitzen.
    • Zahlen: KPIs zu zweit pro Reihe, mit Werten, die schrumpfen, damit sie in 360px passen, statt umzubrechen.
    • Tabellen: Zeilen werden zu Karten (Titel, Untertitel, Badge, bis zu vier Felder); niemand sollte seitlich scrollen müssen, um einen Datensatz zu lesen.
    • Overlays: Menüs, Filter, Datumsauswahl und Detailansichten öffnen sich als Bottom Sheets, die du nach unten ziehen kannst; Bestätigungen bleiben zentriert.
    • Container Queries: In einer Shell misst du den Container (wie das KPI-Grid), damit eine Sidebar nie ein Layout quetscht, das für den Viewport gebaut wurde.
    • Touch: 44px große Ziele bei groben Zeigern (pointer: coarse), keine Aktionen nur per Hover, und jede Geste hat einen sichtbaren Button als Alternative.

    Unser Guide zu Mobile-first React-Komponenten behandelt Bottom Sheets, Touch-Ziele, Safe Areas und Tabellen auf dem Smartphone ausführlich mit Code, und der Guide zur React Bottom Navigation Bar behandelt die Tab Bar und wann die Sidebar allein reicht.

    Solltest du mit einem Next.js Admin Template starten oder selbst bauen?

    Starte mit einem kostenlosen Starter, um den App Router zu lernen, oder wenn du seinen Stack schon nutzt; bau aus Blöcken, wenn Settings, Rollen und Smartphone-Layouts ab dem ersten Tag funktionieren müssen. Bevor du ein „nextjs admin template“ oder ein „shadcn admin dashboard“ auswählst, vergleich diese kostenlosen Ausgangspunkte mit den Blöcken aus diesem Guide (Stand Oktober 2026):

    OptionPreis und LizenzWas du bekommstWas dir überlassen bleibt
    Next.js-Learn-Kurs zum Dashboard (opens in a new tab)KostenlosEin Finanz-Dashboard mit Postgres, Server Actions, Suche und Paginierung in der URL, NextAuth.js und Proxy; seine Rechnungstabelle wird auf dem Smartphone zu KartenSettings, Team und Rollen; er lehrt die Muster und überlässt dir die Screens
    shadcn/ui-Block dashboard-01 (opens in a new tab)Kostenlos, MIT (opens in a new tab)Inset-Sidebar, Summary Cards, ein interaktives Area Chart und eine TanStack-Datentabelle (npx shadcn add dashboard-01)Settings, Team-Seiten, Auth; die Tabelle behält auf dem Smartphone ihre Zeilen und scrollt seitlich
    next-shadcn-dashboard-starter (opens in a new tab)Kostenlos, MITNext.js 16, shadcn/ui auf Base UI, Clerk-Auth, Produkt- und Benutzertabellen mit URL-State, Team-, Billing- und ProfilseitenTeam, Rollen und Billing kommen aus den fertigen Komponenten von Clerk, brauchen also Clerk
    Blöcke von Wingo UI (dieser Guide)Pro: $8 im Monat, $80 im Jahr oder $150 einmaligApp Shell, Admin Shell, die zwei Dashboards, Data Table, Settings Page und Users Admin Page, alle mit Smartphone-LayoutDeine Datenschicht und Auth; die Blöcke sind reine UI

    Wo die kostenlosen Optionen gewinnen: Zum Lernen nimm den Next.js-Kurs, der die Daten- und Auth-Seite lehrt, die dieser Guide auslässt. Wenn du shadcn/ui nutzt und noch heute Abend ein Dashboard brauchst, ist dashboard-01 ein einziger Befehl, und der Code gehört dir. Das ist der kürzeste Weg, falls du nach „nextjs admin dashboard shadcn“ gesucht hast; für interne Tools, die nur am Schreibtisch laufen, ist eine seitlich scrollende Tabelle akzeptabel. Wenn du schon Clerk nutzt, verdrahtet der Starter von Kiranism Organisationen, Teamverwaltung und Billing über die fertigen Komponenten von Clerk. Wingo UI ist seinen Preis wert, wenn Nutzer am Smartphone zählen und du das Verhalten von Settings, Team und Tabelle nicht selbst bauen willst. Für Bibliotheken allgemein vergleicht unsere Übersicht der shadcn-Alternativen vierzehn davon mit Preisen samt Stand-Datum.

    Welche Tutorials bauen die anderen Screens?

    Ein Admin-Bereich ist selten der einzige Screen, den ein Produkt braucht. Diese Tutorials aus Praxis: Screens und Apps bauen den Rest, von den öffentlichen Seiten bis zu ganzen Apps:

    • React Settings Page: Navigation zwischen Bereichen, eine Save Bar für Textfelder, Switches, die selbst speichern, und eine Danger Zone.
    • shadcn Login Page: alle acht Anmeldeansichten, von Magic Link und E-Mail-Codes bis zu Zwei-Faktor-Codes und Passwort-Wiederherstellung.
    • React Onboarding Flow: kurze Willkommensfolien, Berechtigungsabfragen, die erst bei Erlauben laufen, und am Ende ein Installationsangebot.
    • shadcn Pricing Page: ein Umschalter für monatlich und jährlich, Plankarten, eine Vergleichstabelle und ein Layout, das auf dem Smartphone funktioniert.
    • React Checkout Page: ein Warenkorb, vier validierte Schritte, Gutscheine und eine Sticky-Bezahlleiste über dem Home-Indikator.
    • React Invoice Template: ein Editor mit Positionen wie in einer Tabellenkalkulation, eine einzige Summenfunktion für Client und Server und eine Druckansicht.
    • React CRM Template: eine Deal-Pipeline mit gewichteter Prognose, einem Deal-Sheet und einer Firmenseite.
    • React Marketplace Template: Suche, Facetten, Angebotskarten und eine Detailseite, wobei die ganze Suche in der URL bleibt.
    • React Workflow Builder: Trigger, Verzweigungen und KI-Schritte auf React Flow, mit Validierung, Undo und einer Ausführungsansicht.
    • Next.js SaaS Landing Page: die sechs Abschnitte, die eine Landing Page braucht, und die Reihenfolge, in der jeder seine Wirkung entfaltet.
    • Next.js Waitlist Page: ein Countdown, ein Platz in der Warteschlange, Empfehlungslinks und eine Anmeldezahl nur dann, wenn sie echt ist.
    • Next.js Status Page: Uptime-Balken über 90 Tage, Störungen und Wartungen, statisch gerendert und von deinem Monitoring revalidiert.
    • React 404 Seite: Screens für „nicht gefunden“, Serverfehler, „verboten“, Wartung und offline im App Router.
    • Next.js Blog Template: wie wir diesen Blog gebaut haben, aus Markdown, Live-Demos, JSON-LD, Feeds und einem Validator.

    Was solltest du zuerst bauen?

    Bau zuerst die Shell, dann eine Listenseite, dann die Startseite. Die Shell legt Navigation und Smartphone-Layout fest, die jeder Screen erbt, eine Listenseite durchläuft den Serverpfad (URL, Query, Server Actions), den Settings und Team wiederverwenden, und die Startseite ist am einfachsten, sobald echte Daten existieren.

    Die Admin-Blöcke aus diesem Guide gehören zu Wingo UI Pro. Damit startest du mit npx wingo-ui@latest add app-shell (das bringt Sidebar, Topbar, Bottom Tab Bar und die Hooks mit) und setzt sie wie oben in app/(admin)/layout.tsx ein. Die kostenlosen Basics, auf denen sie aufbauen, etwa Button, Card, Badge, Dialog und Toast, installierst du ohne Konto. Für Backoffices, die rumänischen Kunden Rechnungen stellen, gehen zwei Tutorials tiefer: Mehrwertsteuer für e-Factura berechnen rechnet die Mehrwertsteuer pro Satz so, wie ANAF sie prüft, mit einer bearbeitbaren Rechnung, und der Guide zum rumänischen Adressformat baut ein Formular für die Lieferadresse mit Prüfung von Kreis, Ort und Postleitzahl. Weitere Tutorials Screen für Screen findest du unter Praxis: Screens und Apps.

    Komponenten in diesem Artikel

    • 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 Dashboard Page

      A React admin dashboard page: greeting and period, four KPIs, revenue against the previous period, recent orders, alerts and team activity.

      Pro
    • KPI Dashboard

      A React KPI dashboard: metric tiles with deltas, revenue against the last period, sources, funnel, top sellers, invoices and activity.

      Pro
    • Data Table

      A React data table on TanStack Table: search, filters, sorting, bulk actions, pinning, resizing, virtualization and cards on phones.

      Pro
    • 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
    • Stat Card

      A React stat card for dashboards: a label, a big number that counts up, a delta pill that knows good from bad, a sparkline and a goal bar.

      Pro
    • Settings Page

      A React account settings page for a back office: profile, account, notifications, billing, security and a danger zone, with a save bar.

      Pro
    • Users Admin Page

      A React users admin page: team members and invitations with seats, roles with a permissions matrix and a save bar, and the audit log.

      Pro

    Häufige Fragen

    Wie strukturiere ich ein Next.js-App-Router-Projekt für ein Admin Dashboard?

    Leg alle Admin-Routen in eine Route Group wie app/(admin), deren Layout die Shell rendert, und halt die Anmeldung in einer eigenen Gruppe ohne Shell. Seiten sind Server Components, die über einen Data Access Layer nur auf dem Server laden, und jede Route bekommt ihre eigene loading.tsx.

    Gibt es ein kostenloses shadcn Admin Dashboard für Next.js?

    Ja. Stand Oktober 2026 liefert shadcn/ui den kostenlosen Block dashboard-01 (npx shadcn add dashboard-01) mit Sidebar, Summary Cards, einem Chart und einer Datentabelle; Settings, Rollen und ein Smartphone-Layout für die Tabelle bleiben dir überlassen. Der MIT-lizenzierte next-shadcn-dashboard-starter ergänzt Clerk-Auth, Produkt- und Benutzertabellen sowie Team-, Billing- und Profilseiten, die auf den fertigen Komponenten von Clerk aufbauen.

    Soll eine Admin-Datentabelle auf dem Server oder im Browser filtern?

    Filtere im Browser, wenn eine einzige Anfrage alle Zeilen laden kann, die der Nutzer sehen darf; das geht sofort und braucht keine Query-API. Filtere auf dem Server, sobald die Daten über ein paar tausend Zeilen wachsen oder Berechtigungen bestimmen, welche Zeilen jemand sieht, und halt Seite, Sortierung, Suche und Filter in der URL.

    Wohin gehören Auth-Checks in einem Next.js Admin Dashboard?

    In einen Data Access Layer, der nur auf dem Server läuft und den jede Seite und jede Server Action aufruft. Ein Check nur im Layout oder in proxy.ts ist zu schwach: Layouts rendern bei Client-Navigation nicht neu, und Server Actions lassen sich per direktem POST-Request aufrufen.

    Wie mache ich ein Admin Dashboard auf dem Smartphone nutzbar?

    Verschieb die Sidebar in einen Drawer mit Bottom Tab Bar, zeig die KPIs zu zweit pro Reihe, mach aus Tabellenzeilen Karten, öffne Menüs und Detailansichten als Bottom Sheets und halt am unteren Rand nur eine einzige Leiste über der Safe Area. Teste bei 390px auf einem echten Smartphone, denn ein schmal gezogenes Desktop-Fenster hat weder Touch noch Bildschirmtastatur.

    Welche Admin-Komponenten von Wingo UI sind kostenlos?

    Keiner der Admin-Blöcke aus diesem Guide: App Shell, Sidebar, Data Table, Stat Card, KPI Dashboard, Admin Dashboard Page, Settings Page und Users Admin Page gehören zu Pro. Die kostenlosen Basics wie Button, Card, Badge, Dialog, Select, Tabs und Toast installierst du mit der CLI ohne Konto.

    • Next.js
    • Admin Dashboard
    • App Router
    • Datentabelle
    • Mobile UI
    • React

    Teilen

    SR

    Über den Autor

    Serban Rusu

    Gründer von Wingo UI

    Serban Rusu hat Wingo UI gegründet. Er baut die Komponenten, die CLI und den MCP-Server und schreibt über React-UIs für Smartphones und KI-Agents.

    Mehr von Serban
    Weiterlesen

    Ähnliche Artikel

    Alle Artikel
    Vergleiche und Alternativen

    Die beste React Dashboard Library 2026: 7 im Vergleich

    Welche React Dashboard Library passt 2026? 7 Bibliotheken im Vergleich: Datentabelle, Charts, Filter und App Shell, mit Preisen und klaren Empfehlungen.

    SRSerban Rusu·16. Okt. 2026·13 Min. Lesezeit
    Mobile UI mit React

    React Bottom Navigation Bar: Safe Areas und Badges

    React Bottom Navigation Bar in Next.js: 3 bis 5 Tabs, Safe Areas, Badges, die Screenreader vorlesen, Ausblenden beim Scrollen und wann die Sidebar besser passt.

    SRSerban Rusu·9. Okt. 2026·9 Min. Lesezeit
    Praxis: Screens und Apps

    React Settings Page: Navigation, Save Bar und Danger Zone

    Bau eine React Settings Page mit Seitennavigation am Desktop, iOS-Liste auf dem Smartphone, Save Bar für offene Änderungen und abgesicherter Danger Zone.

    SRSerban Rusu·16. Okt. 2026·11 Min. Lesezeit
    Newsletter

    Neue Artikel per E-Mail

    Neue Guides, Tutorials und Vergleiche aus dem Wingo UI Blog, sobald sie erscheinen. Die meisten Artikel sind auf Englisch.

    Kein Spam. Du kannst dich jederzeit abmelden.

    WingoUI

    Animierte, konfigurierbare, mobile-first React-Komponenten und Blöcke. Kopier den Quellcode, pass ihn an und lass deinen Coding-Agenten damit bauen.

    Produkt

    • Komponenten
    • Vorlagen
    • Preise
    • KI-Agenten
    • Theme-Generator

    Ressourcen

    • Doku
    • Blog
    • Changelog
    • Changelog-RSS
    • llms.txt

    Rechtliches

    • AGB
    • Datenschutz
    • Rückerstattung
    • Lizenz
    • Impressum

    Komponenten-Kategorien

    • 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