Nextjs Admin Dashboard Tutorial: 5 Screens, Phone-Ready
Most admin dashboard tutorials stop at a sidebar, four stat cards, a chart and a table that scrolls sideways on a phone. A back office needs more: list pages that filter on the server, a settings page that keeps what someone typed when a save fails, a team page with roles, loading and error states on every route, and a layout that works when the person approving an invoice is on a phone. This nextjs admin dashboard tutorial builds all of it on the Next.js App Router with React 19 and Tailwind CSS v4, screen by screen, with the code. We build Wingo UI and the examples use our blocks, so we are not neutral; the App Router patterns work with any component library, and the last section compares the free starters.
What does a Next.js admin dashboard need?
Five screens and two states: a shell around a home page, list pages, a settings page and a team page, each with a loading and an error state. The React admin dashboard components we use for each, and their phone behavior:
How should you structure the App Router for an admin dashboard?
Put every admin route in one route group with its own layout. A folder in parentheses does not appear in the URL (opens in a new tab), so app/(admin)/clients/page.tsx serves /clients while the group's layout.tsx wraps it in the shell. Sign-in lives in its own group and never sees the sidebar.
Three rules hold it together:
- Pages fetch, views render. Each
page.tsxis a server component that loads data. The blocks are client components, so function props (a save handler, a router call) live in a small*-view.tsxnext to the page. - Auth lives in the data access layer. Layouts do not re-render on navigation (opens in a new tab), so a layout check misses route changes. Call
verifySession()from aserver-onlymodule in every page and every Server Action; the data security guide (opens in a new tab) says to treat each Server Action as reachable by a direct POST request. - The URL holds the view. Period, filters, page, open tab: if it changes what the screen shows, it goes in the search params, so a link reproduces the screen.
How do you build the app shell for a Next.js admin dashboard?
Render one AppShell in the group's layout with a Sidebar and a Topbar, and read the sidebar's cookie on the server so the first paint is right. The shell shows the sidebar beside the page on desktop, and a drawer plus a bottom tab bar on phones. It switches with CSS, so the server HTML is already right.
First, the router wiring: the Wingo UI navigation links render through LinkProvider, so one client provider turns them all into next/link:
The sidebar and the tab bar share the NavItem shape, so one file feeds both:
And the layout, a server component:
Mod+B (Cmd on a Mac, Ctrl elsewhere) folds the sidebar into a 64px icon rail and stores that in the app-shell-collapsed cookie. Without the server read, a user who collapsed it sees it expanded on every first paint and then snap to the rail after hydration. Use the literal cookie name on the server: the persistedCookieName helper lives in a hook file that imports client-only React hooks, which a server layout cannot import. Below the breakpoint the sidebar becomes a drawer that closes on navigation, Escape, a tap outside or a swipe back, and focus returns to the menu button. A "Skip to content" link is the first Tab stop.
We pass mobileBreakpoint="lg" (1024px) on purpose. At md (768px), an 820px tablet in portrait keeps a 256px sidebar and 564px for the page, too little for a table. At lg it gets the phone layout and the full width.
Keep the awaits in this layout cheap: a slow await here holds every page behind it (opens in a new tab). A cookie read and a cached session lookup are fine; a dashboard query is not.
For comparison, the shadcn/ui Sidebar (as of October 2026) stores its open state (opens in a new tab) in a sidebar_state cookie for seven days and renders a Sheet when its useIsMobile hook reports a phone. It has no bottom tab bar, so on a phone every destination is two taps away.
For the whole back office layout in one block, the Admin Shell adds a workspace switcher, a global search on /, notifications, a command menu on Mod+K and a five-tab bar whose More tab opens the drawer. Our React command palette guide covers that menu's shortcuts, nested pages and phone sheet.
How do you build the dashboard home with KPIs?
Fetch the numbers in the page for the period in the URL, and pass them to a dashboard block as props. The Admin Dashboard Page is the home of an orders business (four KPIs, revenue against the previous period, recent orders, what needs attention, team activity); the KPI Dashboard suits a B2B or analytics app.
The page reads the period, defaults to the last 30 days and fetches:
The view writes a new period back to the URL and sends order actions to Server Actions:
The period is uncontrolled on purpose (defaultPeriod): the date field updates the moment someone applies a range, and the panels follow when the server answers. onOrderAction is optimistic: the status changes at once, the row shows a spinner while the promise runs, and a rejection rolls it back with an error toast, which is why the handler throws on an error. One catch: after a cancel, the toast's Undo restores the row on screen only and calls none of your code, so the next server render shows the order as cancelled again. Rewrite labels.cancelDescription so users know a cancel is final; its default text promises an email and a refund, which only your server can keep.
$ npx wingo-ui@latest add admin-dashboard-pageOn a phone the KPIs sit two by two, recent orders become cards with Load more, and the date field and the order details open as bottom sheets you drag down to close.
When should you use the KPI Dashboard instead?
When the home is about analytics more than orders: revenue over time, sales by source, a conversion funnel, top salespeople and invoices. It takes one data object and hides a widget whose data is missing, so it fits a company with no funnel. It has the controls an analytics page needs: onRefresh (a promise spins the button and announces the update), onExport with CSV and PDF, a widgets list to pick and order panels, and widgetErrors with onWidgetRetry to show an error and a Try again button inside the one widget whose query failed, so the rest of the page stays up.
Its grid uses container queries: one column, two from 672px and three from 1152px of its own width. Inside a shell that matters, because a 1280px window with a 256px sidebar leaves about 1000px, and viewport breakpoints would lay the grid out for space it does not have. The DashboardWidget and KpiDashboardGrid parts are exported for your own panels.
Can you render KPI cards from a server component?
Yes, as long as every prop is data. The Stat Card takes numbers, strings and icons, so a server component can render a row directly:
The server HTML holds the final numbers, the count-up runs on the client when a card scrolls into view, and screen readers hear one sentence with the final value and the change. invertTrend makes a drop in time to payment green. Two limits: a function prop such as formatDelta needs a client file, and href renders a plain anchor (a full page load in Next.js), so linked tiles need a client wrapper whose onClick calls event.preventDefault() and then router.push.
How do you build list pages with a server-side data table?
Run the Data Table in server mode, keep its query in the URL, and let the page fetch one page at a time. In mode="server" the table does no sorting, filtering or paging itself: data is the current page, rowCount the total, and every change arrives in onQueryChange as one object (page, pageSize, sorting, globalFilter, columnFilters). One module translates it to and from the search params:
The page parses the URL and queries one page:
The view starts from the URL and writes changes back in a transition, so isPending drives the refreshing bar:
Details that save debugging time: onQueryChange does not fire on the first render (the server already sent that page), the search arrives debounced (300ms), and the page resets to 1 when the search, a filter or the sort changes. The table is uncontrolled (default* props), so the search box never lags behind a server round trip. The trade-off: the back button changes the data while the toolbar keeps the last query. If your users live on the back button, control page, sorting, globalFilter and columnFilters from the URL instead.
$ npx wingo-ui@latest add data-tableOn a phone the columns become cards: mobile: "title" is the heading, subtitle the line under it, meta the badge in the corner. Sorting, filters and the row menu open as bottom sheets, and with selectionMode on, a long press starts selection. Server mode keeps a compact pager on phones; client mode uses Load more.
Server mode is not always the right call. When one request can load every row a user may see, client mode filters instantly and needs no query API. We switch to server mode past a few thousand rows, or when permissions decide which rows exist for a user. To build the table yourself on TanStack Table instead, with the same server pagination, follow our TanStack Table v9 data table tutorial.
How do you build a settings page that never loses input?
Let one block own the drafts, and save through Server Actions that return their errors. The Settings Page has six sections (profile, account, notifications, billing, security, danger zone). A text change slides up a save bar with Discard and Save (or Mod+S), switches save on their own, a failed save keeps the draft and shows the message, and leaving a section with unsaved changes asks first.
Next.js recommends modeling expected errors as return values (opens in a new tab) in Server Functions, and in production the client does not get the original message of a server error, so a thrown "This workspace address is taken" would arrive as a generic error:
The view turns a returned error into the rejection the block listens for, and replaces the block's demo checks with real ones:
Pass every data prop from the page: anything you leave out falls back to the block's sample data. The security defaults are demo values on purpose. Without verifyEmailCode and verifyTwoFactorCode, the code 123456 passes; without twoFactorSecret, twoFactorUri and backupCodes, the setup dialog shows sample ones; and the default codeHint label reads "In this demo, the code is 123456." Generate the secret and backup codes on the server for the signed-in user.
The dialogs take handlers with the same promise contract (onEmailChange, onPasswordChange, onTwoFactorChange, onSessionRevoke, onPlanChange, onDeleteAccount). One naming detail: the section ids are internal (profil, cont, notificari, facturare, securitate, pericol), so map them to your own segments if each section gets a URL through onSectionChange.
$ npx wingo-ui@latest add settings-pageFrom 768px the sections are a column on the left (or tabs with nav="tabs"). On a phone each section pushes in with a back button and a swipe back, dialogs open as bottom sheets, and the save bar is pinned above the tab bar. The block validates each form before it calls onSave: invalid fields turn red and the first one takes focus, the pattern our React form components guide covers field by field.
How do you build the team and roles page?
Treat every control on it as a request the server can refuse. The Users Admin Page has three tabs: Members (the team and pending invitations against your seats, a role picker per row), Roles and permissions (a matrix with a save bar) and Audit log (a filterable history with CSV export). The server decides who may manage and enforces the rules the interface shows:
The page computes canManage from the session and passes it with the data. With canManage={false} the block turns read-only and says why, which beats hiding the page from people who only need to see who has access.
The interface guards the obvious mistakes: the last administrator cannot be demoted, nobody removes themselves or the owner, and invitations hold a seat. Those guards are for the person clicking; your actions enforce them again, because a Server Action cannot tell which button sent the request. Likewise the block adds this page's changes to the top of the audit log as feedback, but the real audit rows are the ones your server writes in the same transaction.
On a phone, members become cards with the role picker inside, the invite dialog is a bottom sheet, and the matrix shows one role at a time with chips, because five role columns do not fit 390px. The parts also ship alone: Members Table, Permissions Matrix and Audit Log.
How do you handle loading and error states in the admin?
Give every route a loading.tsx that renders the same component with its loading prop, and the group one error.tsx. loading.js (opens in a new tab) shows its fallback immediately on navigation while the shell stays interactive, and a skeleton in the real layout means nothing jumps when the data arrives:
The Data Table draws skeleton rows in its column layout (cards on phones), and the dashboard, settings and team blocks have the same prop. A route without its own loading.tsx falls back to the nearest parent's, so one group-level skeleton would flash the same layout before every page.
For errors, Next.js 16.3 made the retry prop of error.js (opens in a new tab) stable; it re-fetches and re-renders the segment:
Empty states belong in the components: the Data Table's emptyState for a list with no rows yet and noResultsState when filters match nothing, so "No clients yet" and "Nothing matches" never get mixed up.
How do you make every admin screen usable on a phone?
Change behavior as well as layout, and check each screen at 390px on a real phone. People open a back office on phones to approve an expense, check an order or answer a client. The checklist we hold every admin screen to:
- Navigation: everything in a drawer behind the menu button, plus a tab bar for the three to five daily destinations; tablets in portrait get the phone layout.
- One bar on the bottom edge: the tab bar pads the home indicator and publishes its height as
--bottom-bar-offset, and the settings and team save bars read it to sit above the tab bar. - Numbers: KPIs two per row, with values that shrink to fit 360px instead of wrapping.
- Tables: rows become cards (title, subtitle, badge, up to four fields); nobody should scroll sideways to read one record.
- Overlays: menus, filters, date pickers and detail views open as bottom sheets you can drag down; confirmations stay centered.
- Container queries: inside a shell, measure the container (as the KPI grid does), so a sidebar never squeezes a layout built for the viewport.
- Touch: 44px targets on coarse pointers, no hover-only actions, and every gesture backed by a visible button.
Our mobile-first React components guide covers the bottom sheets, touch targets, safe areas and phone tables in depth, with code, and the React bottom navigation bar guide covers the tab bar and when the sidebar alone is enough.
Should you start from a Next.js admin template or build it yourself?
Start from a free starter to learn the App Router or when you already use its stack; build from blocks when settings, roles and phone layouts must work on day one. Before you pick a "nextjs admin template" or a "shadcn admin dashboard", compare these free starting points with the blocks in this guide (as of October 2026):
Where the free options win: to learn, take the Next.js course, which teaches the data and auth side this guide skips. If you run shadcn/ui and need a dashboard tonight, dashboard-01 is one command and you own the code, which makes it the shortest path if you searched for "nextjs admin dashboard shadcn"; for desk-only internal tools a sideways-scrolling table is acceptable. If you already use Clerk, the Kiranism starter wires organizations, team management and billing through Clerk's prebuilt components. Wingo UI earns its price when phone users matter and you do not want to build the settings, team and table behaviors yourself. For libraries in general, our shadcn alternatives comparison covers fourteen with dated prices.
What should you build first?
Build the shell first, then one list page, then the home. The shell sets the navigation and phone layout every screen inherits, a list page exercises the server path (URL, query, Server Actions) that settings and team reuse, and the home is easiest once real data exists.
The admin blocks in this guide are part of Wingo UI Pro. With it, start with npx wingo-ui@latest add app-shell (it brings the Sidebar, Topbar, Bottom Tab Bar and the hooks along) and drop it into app/(admin)/layout.tsx as above. The free basics they build on, such as the Button, Card, Badge, Dialog and Toast, install without an account. More screen-by-screen tutorials live in Build it: screens and apps.
FAQ
How should I structure a Next.js App Router project for an admin dashboard?
Put every admin route in a route group such as app/(admin) whose layout renders the shell, and keep sign-in in a separate group without it. Pages are server components that fetch through a server-only data access layer, and each route gets its own loading.tsx.
Is there a free shadcn admin dashboard for Next.js?
Yes. As of October 2026 shadcn/ui ships a free dashboard-01 block (npx shadcn add dashboard-01) with a sidebar, summary cards, a chart and a data table, but settings, roles and a phone layout for the table are left to you. The MIT next-shadcn-dashboard-starter adds Clerk auth, product and user tables, and team, billing and profile pages built on Clerk's prebuilt components.
Should an admin data table filter on the server or in the browser?
Filter in the browser when one request can load every row the user may see; it is instant and needs no query API. Filter on the server when the data grows past a few thousand rows or permissions decide which rows a user sees, and keep the page, sort, search and filters in the URL.
Where do auth checks go in a Next.js admin dashboard?
In a server-only data access layer that every page and every Server Action calls. A check in the layout or in proxy.ts alone is too weak: layouts do not re-render on client navigation, and Server Actions can be called with a direct POST request.
How do I make an admin dashboard usable on a phone?
Move the sidebar into a drawer with a bottom tab bar, show KPIs two per row, turn table rows into cards, open menus and detail views as bottom sheets, and keep a single bar on the bottom edge above the safe area. Test at 390px on a real phone, because a narrowed desktop window has no touch and no on-screen keyboard.
Which Wingo UI admin components are free?
None of the admin blocks in this guide: the App Shell, Sidebar, Data Table, Stat Card, KPI Dashboard, Admin Dashboard Page, Settings Page and Users Admin Page are Pro. The free basics, such as Button, Card, Badge, Dialog, Select, Tabs and Toast, install with the CLI without an account.
- Next.js
- Admin dashboard
- App Router
- Data table
- Mobile UI
- React