Freeframe

Documentation

Dashboard

v1.1.6Updated Oct 18, 2026A polished admin dashboard with live charts, data tables and settings.

Overview#

Dashboard is a complete admin interface for the fictional outdoor-gear shop behind Northstar Admin: KPI cards and charts, analytics, a full orders table, customers with a detail panel, a product catalogue with inventory, settings (profile, team, billing, notifications) and a sign-in screen, in light and dark themes. Charts are SVG React components with no chart library. This is the Next.js 16 (App Router) + React 19 + TypeScript + Tailwind CSS v4 version, version 1.1.3. It exports to static files. See it live at freeframe.dev/templates/dashboard.

Quick start#

Requirements: Node.js 24 or newer and pnpm (npm works too).

sh
pnpm install
pnpm dev          # http://localhost:3000
Command What it does
pnpm dev Development server at http://localhost:3000
pnpm build Static export to out/
pnpm start Serve out/ locally (npx serve out)
pnpm lint ESLint
pnpm typecheck Generate route types, then type-check with tsc (TypeScript 7)
pnpm build:demo Build for /demos/nextjs/dashboard into demo/ (only used for the freeframe.dev demo)

Pages#

Route File What's on it
/ src/app/(app)/page.tsx KPI cards with sparklines, revenue chart (7d / 30d / 12m), traffic donut, recent orders, activity
/analytics src/app/(app)/analytics/page.tsx Visitors line chart, customer-type bars, checkout funnel, order heatmap, top pages, countries
/orders src/app/(app)/orders/page.tsx Orders table: status tabs, search, sorting, pagination, selection and bulk actions
/customers src/app/(app)/customers/page.tsx Customer list with segments and search, slide-over detail panel with notes
/products src/app/(app)/products/page.tsx Catalogue with category filter, grid/table view, inventory
/settings src/app/(app)/settings/page.tsx Profile, team (invite modal), billing, notifications; /settings#billing opens a tab
/login src/app/login/page.tsx Sign-in screen (demo)
404 src/app/not-found.tsx Not-found page

(app) is a route group: its layout.tsx wraps those pages in the dashboard chrome (sidebar, top bar) without adding /app to the URL. The sign-in and 404 pages use the root layout only.

Project structure#

dashboard/
├─ src/
│  ├─ app/
│  │  ├─ layout.tsx             fonts, metadata, theme/sidebar boot script, toast provider, back to top
│  │  ├─ globals.css            brand and chart tokens, component classes
│  │  ├─ icon.svg               favicon
│  │  ├─ (app)/layout.tsx       dashboard chrome (AppShell) and scroll reveals
│  │  ├─ (app)/<page>/page.tsx  overview, analytics, orders, customers, products, settings
│  │  ├─ login/page.tsx
│  │  └─ not-found.tsx
│  ├─ components/
│  │  ├─ shell/                 app-shell (sidebar, drawer, top bar), dropdown, global-search, theme-toggle
│  │  ├─ charts/                line-chart, bar-chart, donut, funnel, heatmap, sparkline, tooltip
│  │  ├─ pages/                 interactive page sections (orders table, customers list, products,
│  │  │                         settings tabs, revenue and visitors cards, login form)
│  │  ├─ modal.tsx, toast.tsx, ui.tsx, icons.tsx, reveal.tsx, query-param.tsx, back-to-top.tsx
│  ├─ fonts/                    Inter and JetBrains Mono (Latin .woff2) with their OFL licences
│  ├─ data/demo.ts              all demo data (orders, customers, products, chart series, search index)
│  └─ lib/
│     ├─ use-validation.tsx     form validation hook
│     ├─ format.ts              number/date formatting and chart maths
│     ├─ metadata.ts            pageMetadata() helper
│     └─ dom.ts                 focus trap and scroll lock helpers
├─ public/og-image.png          1200×630 social sharing image
├─ next.config.ts, eslint.config.mjs, postcss.config.mjs, tsconfig.json
├─ package.json, pnpm-workspace.yaml, pnpm-lock.yaml
└─ README.md, DOCS.md, CHANGELOG.md

Customising#

Brand and colours#

src/app/globals.css defines the brand-* colour scale (indigo by default) in @theme, used for buttons, links, the active navigation item and focus rings. Chart colours are CSS variables defined for light mode and again for dark mode: --chart-1 to --chart-5, --chart-grid, --chart-text and --chart-surface. Charts read them at paint time, so they follow the theme automatically. The browser theme colours are the viewport.themeColor export in src/app/layout.tsx.

Typography#

Inter and JetBrains Mono are self-hosted with next/font/local in src/app/layout.tsx (variables --font-inter and --font-jetbrains-mono), from the Latin .woff2 files in src/fonts/: the same files Google Fonts serves to the HTML version, so text sets exactly like it. Their SIL Open Font License texts are the *-OFL.txt files beside them. To change a font, add its .woff2 (and licence) to src/fonts/ and point src at it, or import a family from next/font/google, keeping the variable name; or update the --font-sans / --font-mono tokens in globals.css.

Content#

All demo data lives in src/data/demo.ts:

Export What it is
TODAY The fixed "current" date of the demo (3 October 2026, UTC)
NAMES, STATUS_TONE Customer names; order status → badge colour
PRODUCTS Products (name, SKU, category, price, stock, capacity, units sold, hue, icon)
ORDERS 48 generated orders
CUSTOMERS Customers derived from NAMES and their orders, with a Segment
REVENUE Revenue series for the 7d, 30d and 12m ranges
TRAFFIC_SOURCES, VISITOR_SERIES, CUSTOMER_TYPE_SERIES, FUNNEL Analytics chart data
heatmapDays(), heatmapTotal() Order heatmap data
SEARCH_INDEX Everything global search can find

Add a product:

ts
{ name: "Summit Trail Cap", sku: "AP-402", cat: "Apparel", price: 29, stock: 140, cap: 200, sold: 610, hue: "emerald", icon: "jacket" },

hue and icon must be values of the Hue and ProductIcon types. To use real data, replace these exports with data from your API. With the static export, fetch it in client components (useEffect + fetch); if you remove the static export, page components can await it on the server instead. Set TODAY to new Date() once data is real.

Images and artwork#

There are no photos: avatars are generated initials, product images are SVG icons on coloured tiles (src/components/pages/products-catalog.tsx), and charts are SVG. The favicon is src/app/icon.svg; the social image is public/og-image.png (1200×630).

src/components/shell/app-shell.tsx holds the sidebar (the WORKSPACE link list plus the Settings link), the mobile drawer, the top bar with search, notifications (NOTIFICATIONS), the account menu and the theme toggle. To add a page:

  1. Create src/app/(app)/<route>/page.tsx with export const metadata = pageMetadata("Title", "Description").
  2. Add it to WORKSPACE.
  3. Add it to SEARCH_INDEX in src/data/demo.ts.

Dark mode#

bootScript in src/app/layout.tsx applies the saved theme (ns-theme) and sidebar state (ns-sidebar) before the first paint. ThemeToggle (src/components/shell/theme-toggle.tsx) flips the dark class and saves the choice. To default to light, remove ||(!t&&matchMedia('(prefers-color-scheme: dark)').matches) from bootScript.

Features#

On wide screens the sidebar collapses to icons (saved as ns-sidebar); collapsed links keep accessible names and tooltips. On small screens it's a drawer: focus is trapped, Escape or the backdrop closes it and focus returns to the menu button.

Ctrl K / ⌘ K or / focuses search (src/components/shell/global-search.tsx). Results come from SEARCH_INDEX and are navigable with arrow keys and Enter. Entries link to filtered pages such as /orders?q=NS-4821; pages read the query with the QueryParam component, which is wrapped in Suspense so they stay statically rendered.

Dropdown menus close with Escape or a click outside. Modal (src/components/modal.tsx) traps focus and returns it. Toasts come from ToastProvider: call useToast() to get the toast(message) function, or use ToastButton / ToastLink. The back-to-top button moves up while a toast is visible.

Charts#

src/components/charts/ contains LineChart, BarChart, Donut, Funnel, Heatmap and Sparkline, all responsive SVG with hover tooltips. LineChart is also keyboard-accessible: focus it and use the Left/Right arrow keys to move between points, Escape to hide the tooltip.

tsx
<LineChart
  ariaLabel="Revenue, last 30 days"
  labels={REVENUE["30d"].labels}
  series={[{ name: "This period", color: "var(--chart-1)", values: REVENUE["30d"].now, area: true }]}
  format={(v) => money0.format(v)}
/>

Orders table#

OrdersTable (src/components/pages/orders-table.tsx) has status tabs with counts, search (also from ?q=), sortable columns with aria-sort, PER_PAGE (10) rows per page, row selection and a bulk-action bar (mark as paid, export, clear). Export is a demo toast.

Customers and products#

CustomersList filters by segment and search and opens a slide-over detail panel with a validated notes form. ProductsCatalog filters by category and switches between grid and table views, remembered as ns-products-view.

Settings#

SettingsTabs keeps the selected tab in the URL hash (/settings#team, /settings#billing) and follows hash changes. Tabs support arrow keys, Home and End. It includes the profile form with a bio counter, the team list with an invite modal, plan cards with a switch button, invoices and notification switches.

Back to top button#

BackToTop (mounted in src/app/layout.tsx) appears after scrolling about one screen, returns to the top (instantly with reduced motion), moves focus to #main, lifts above a visible toast (anything matching .toast or [data-back-to-top-avoid]), steps aside while an in-page "Back to top" link or an element marked data-back-to-top-hide is in the way, and is hidden when printing. Its styles are .back-to-top in src/app/globals.css. Remove <BackToTop /> and those styles to drop it.

Forms#

Forms use the useValidation() hook from src/lib/use-validation.tsx:

tsx
const form = useValidation();

<form noValidate onSubmit={form.submit((el) => toast("Profile saved"))}>
  <input className="field" type="email" {...form.field("profile-email", { label: "Email", required: true, email: true })} />
  {form.error("profile-email")}
</form>

field(id, rules) returns the props for an input (id, name, required, aria-invalid, aria-describedby and blur/input validation); rules are label, required, email and minLength. submit(onValid) validates every registered field, focuses the first invalid one, and otherwise calls onValid(formElement).

Form Component Behaviour
Sign in LoginForm (src/components/pages/login-form.tsx) Email and password (8+ characters), then redirects to /
Profile SettingsTabs Profile fields; shows a "Profile saved" toast
Invite member SettingsTabs (modal) Email and role; adds the member to the team list
Customer note CustomersList (panel) Note text; shows a toast

Nothing is saved or sent. To save for real, post the data in onValid:

tsx
onSubmit={form.submit(async (el) => {
  const res = await fetch("/api/profile", { method: "POST", body: new FormData(el) });
  toast(res.ok ? "Profile saved" : "Something went wrong");
})}

With the static export, point fetch at your existing API. To write the API inside this project, remove output: "export" from next.config.ts and add Route Handlers (e.g. src/app/api/profile/route.ts) or Server Actions. Replace the timeout in LoginForm with your authentication provider, and protect the dashboard routes on the server (for example with a session check in a layout or proxy.ts); hiding the link to a page isn't protection.

Saved data (localStorage)#

Key Values Used for
ns-theme "dark" or "light" Colour theme
ns-sidebar "collapsed" or "open" Desktop sidebar state
ns-products-view "grid" or "table" Products page view

Values are compared with the exact expected strings when read, so a missing or tampered value falls back to the default (system theme, open sidebar, grid view).

SEO and sharing#

  • Every page uses pageMetadata(title, description) from src/lib/metadata.ts, which sets the title (Title · Northstar Admin), description, Open Graph and Twitter card fields and adds the base path to the social image URL.
  • metadataBase in src/app/layout.tsx reads SITE_URL (default https://northstar.example). Set it at build time so the image URL is absolute: SITE_URL=https://example.com pnpm build.
  • An admin area usually shouldn't be indexed: add robots: { index: false } to pageMetadata and a src/app/robots.ts that disallows everything.

Accessibility#

Built in: a skip link, landmarks, visible focus rings, focus-trapped drawer and modals, menus that close with Escape, tabs with arrow keys / Home / End, keyboard-readable line charts, aria-sort on sortable columns, aria-pressed toggles and role="switch" switches, form errors linked with aria-describedby, reduced-motion support, and WCAG AA contrast in both themes.

When editing, keep one <h1> per page, label every control, and give each chart a descriptive ariaLabel.

Building and deploying#

sh
pnpm build        # static site in out/
pnpm start        # preview out/

next.config.ts sets output: "export", images.unoptimized and basePath from DEMO_BASE_PATH (for sub-folder hosting, e.g. DEMO_BASE_PATH=/admin pnpm build). Deploy out/ to any static host, or import the repository into Vercel. For a real admin area, remove the static export, add authentication and your API, and deploy to a host that runs Next.js. demo/, if present, is the freeframe.dev demo build and can be deleted.

TypeScript 7 setup#

@typescript/native provides TypeScript 7's tsc for pnpm typecheck; typescript is aliased to @typescript/typescript6 because typescript-eslint and parts of the Next.js toolchain need the TypeScript 6 API. Replace both with typescript@7 once typescript-eslint supports it. eslint.config.mjs sets settings.react.version, which ESLint 10 needs.

Troubleshooting#

  • useSearchParams build error after adding a page. Read query parameters with QueryParam (it adds the Suspense boundary static export needs).
  • Social previews have no image. Build with SITE_URL set to your domain.
  • A chart is blank after you change its data. Every series needs as many values as there are labels.
  • The sidebar stays collapsed. Clear ns-sidebar from localStorage or click the collapse button.
  • Dates look stuck in October 2026. TODAY is fixed for the demo; set it to new Date().
  • ESLint crashes with getFilename is not a function. Keep settings.react.version in eslint.config.mjs.
  • next build fails after adding an API route. Remove output: "export".

License#

MIT. The code and artwork are free for personal and commercial use; the download includes the license text in LICENSE.txt. Inter and JetBrains Mono are licensed under the SIL Open Font License.

Search documentation

Search the guides and every template's documentation