Overview#
Dashboard is a polished admin dashboard UI, built as a WordPress block theme. The fictional product
is Northstar Admin, the back office of Trailhead Co., an outdoor-gear store. The theme has an
overview with KPI cards and an interactive revenue chart, an analytics page (line, stacked bar,
funnel and heatmap charts), an orders table with search, status tabs, sorting, pagination and bulk
actions, customers with a detail panel, a product catalogue with grid and table views and inventory
levels, tabbed settings with dialogs and switches, a sign-in screen and a 404 page, with a
collapsible sidebar and light and dark themes. It is version 1.0.0 (see template.json and
style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the WordPress version of the
HTML template of the same name. See it live at
freeframe.dev/templates/dashboard.
Dashboard is a front end: its numbers, orders, customers and products are a demo data set in
assets/js/main.js, and the charts and tables are drawn by that script, as in the HTML version.
Use it to prototype or present an admin UI, or as the front of an app whose data comes from your
own API. Page headings and descriptions, cards and their labels, KPIs, legends, the activity feed,
plans and the settings copy are core WordPress blocks, so you edit them in the block editor.
Colours and fonts are design tokens in theme.json: change them in Appearance > Editor > Styles and
the whole site follows.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
dashboard.zipfrom freeframe.dev/templates/dashboard (the WordPress tab). - In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Dashboard.
- Go to Appearance > Dashboard setup and click Create the demo pages. This creates the Overview, Analytics, Orders, Customers, Products, Settings and Sign in pages from the theme's page patterns (Sign in with the Full screen (sign-in) template) and sets Overview as the front page (Settings > Reading). Pages that already exist with the same slug are left alone, so it is safe to click it again.
- Visit your site. Edit any page in Pages, and the sidebar, top bar, footer and templates in Appearance > Editor.
You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.
To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:
pnpm install
pnpm dev # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400pnpm dev signs you in as admin (password password) and creates the demo pages. The site is
rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the
WordPress admin are not kept.
Pages#
Each page is a page pattern (in Patterns > Dashboard pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Dashboard.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Overview | / |
page-home.php |
overview-header, kpis (KPI cards with sparklines), revenue-traffic (revenue chart, traffic donut), recent-activity (recent orders, activity feed, top products) |
| Analytics | /analytics/ |
page-analytics.php |
analytics-header, traffic-summary, visitors-chart, revenue-funnel (stacked bars, checkout funnel), order-heatmap, top-pages (top pages, visitors by country) |
| Orders | /orders/ |
page-orders.php |
orders-header, orders-tabs (status tabs), orders-table |
| Customers | /customers/ |
page-customers.php |
customers-header, customer-summary, customers-table, customer-panel (the detail panel) |
| Products | /products/ |
page-products.php |
products-header, inventory-summary, products-tools, products-grid, products-table, products-empty |
| Settings | /settings/ |
page-settings.php |
settings-header, settings-tabs, settings-profile, settings-team, settings-billing, settings-notifications, invite-modal, delete-modal |
| Sign in | /login/ |
page-login.php |
sign-in-top (logo and theme switch), sign-in (form), sign-in-footer; the right-hand panel is the sign-in-aside pattern in the Full screen template |
| 404 | any missing URL | templates/404.html |
404 |
The script finds the pages by their slugs: if you rename one, change its URL in
freeframe_dashboard_page_urls() in inc/setup.php too (search results, order links and the
sign-in redirect use it).
Templates#
| Template | Used by | Layout |
|---|---|---|
page.html |
pages | the app: sidebar, top bar, the page content in <main>, footer |
page-full-screen.html (Full screen (sign-in)) |
Sign in | two columns: the page content and the sign-in-aside pattern; no sidebar or top bar |
page-with-title.html (Page with title) |
text pages you add | the app layout with the page title and text in a readable column |
single.html, index.html, archive.html, search.html |
posts, blog, archives, search | the app layout with a readable column |
404.html |
missing pages | the minimal header and footer around the 404 pattern |
Project structure#
freeframe-dashboard/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, font sizes, layout widths, button; templates and parts
functions.php loads the files in inc/
inc/setup.php stylesheet and script (with its strings and page URLs), editor styles,
dark mode and sidebar script, pattern categories
inc/icons.php registers assets/icons/*.svg for the Icon block
inc/icon-labels.php the icons' names shown in the editor
inc/block-styles.php button block styles
inc/site-setup.php Appearance > Dashboard setup: creates the demo pages
templates/ see Templates above
parts/ sidebar.html, header.html, footer.html, header-minimal.html,
footer-minimal.html (each shows the pattern of the same name)
patterns/ every section and page (PHP files with block markup), the sidebar,
top bar, footers, the toast and the sign-in panel
assets/css/theme.css the compiled stylesheet (do not edit: built from src/)
assets/css/editor.css the same for the block editor (generated by src/editor-css.mjs)
assets/js/main.js the app: demo data, charts, tables, menus, dialogs, forms
assets/icons/ SVG icons for the Icon block
assets/img/ logo, favicon, social image
assets/fonts/ Inter and JetBrains Mono, with their licenses
src/theme.css the Tailwind CSS source of assets/css/theme.css
src/sidebar-icons.css the sidebar menu's icons (CSS masks)
src/icons.css icons drawn before or after text with CSS (generated)
src/editor-css.mjs writes assets/css/editor.css after each build
languages/ freeframe-dashboard.pot, the template for translations
blueprint.json WordPress Playground setup used by `pnpm dev`
screenshot.png the picture shown in Appearance > Themes
package.json the CSS build and the local WordPress (Node.js tooling)
readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txtCustomising#
Brand and colours#
The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Colours | Used for |
|---|---|
| Brand 50 to Brand 950 | the indigo of primary buttons, links, the current menu item, badges and focus rings |
| Zinc 50 to Zinc 950, White, Black | neutrals, for your own blocks |
| Base and Contrast | the page background (#fafafa) and text; they switch to dark values in dark mode |
The theme's stylesheet reads the Brand colours from the palette, so changing Brand 600 in Styles
recolours every primary button and accent at once. Pick a full ramp of shades (light 50 to dark
950) so hover states and dark mode keep their contrast. The chart colours are CSS variables
(--chart-1 to --chart-5, --chart-grid, --chart-text, --heat-0) at the top of
src/theme.css, with a second set for dark mode; the script reads them.
Typography#
Two fonts are bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google: Inter (variable, 400 to
700; preset sans) for everything and JetBrains Mono (400 and 500; preset mono) for order
numbers, SKUs and codes. To use other fonts, install them in Styles > Typography > Fonts (the
Font Library) and select them, or replace the fontFace entries in theme.json. Font sizes are
the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale).
Content#
Edit pages in Pages: click any text to change it. Sections are groups; open List View (the stacked lines icon at the top left) to select a section, move it, duplicate it or delete it. To add a section from another page, use the block inserter (+) > Patterns > Dashboard.
What the script draws or drives is in Custom HTML blocks: edit those in the block's HTML view,
and keep their ids and data- attributes.
| Pattern | Custom HTML | What it is |
|---|---|---|
*-header |
the action buttons | buttons that show a demo message (data-toast-msg) or link to a page |
kpis, revenue-traffic, visitors-chart, revenue-funnel, order-heatmap |
sparklines, charts, their range buttons and legends | drawn from the demo data, see Charts |
recent-activity, top-pages, orders-table, customers-table, products-* |
tables, lists, search, filters, pagination | filled from the demo data |
orders-tabs, settings-tabs |
tabs | see Settings |
settings-* |
forms, plan and card buttons, the team list, notification switches | see Forms |
customer-panel, invite-modal, delete-modal |
the panel and dialogs | opened by the script |
sign-in, sign-in-top, sign-in-aside |
the sign-in form and buttons, the theme switch, the panel's chart | |
header, sidebar |
search, the theme switch, notifications and account menus; the sidebar's buttons |
A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the script or stylesheet reads:
ff-tabpanelon each settings section (the script gives it its tab panel role and label), andff-hiddenon the sections hidden until their tab is chosen. In the editor hidden sections are shown with a dashed outline.ffd-stat-products,ffd-stat-low,ffd-stat-out,ffd-stat-valueon the inventory numbers andffd-current-planon the plan name: the script writes into them (blocks can't storedata-attributes, so the script turns these classes back into them).ffd-plan-cardmarks each plan card.revealfades a card in as it scrolls into view;card,card-headandcard-titleare the card styles;badge badge-green(-amber,-red,-zinc,-blue) are the coloured badges.
The demo data
All demo data is generated in section 2 ("Demo data") of assets/js/main.js: TODAY (the fixed
date the demo is built around), NAMES, CITIES, CHANNELS, STATUSES, PRODUCTS (name, SKU,
category, price, stock, capacity, units sold, colour and icon), ORDERS (48 generated orders) and
CUSTOMERS (derived from the names and orders). Chart series are in section 8. To use real data,
replace these lists with data from your API (for example fetch('/api/orders').then(r => r.json()))
and call the same render code once it arrives; set TODAY to new Date(). Make these changes in
a child theme (copy assets/js/main.js there and load it instead), or they are overwritten when
you update the theme. The static numbers in the KPI cards, the analytics summary, the activity
feed and the settings are blocks: edit them in the editor.
Images and artwork#
There are no photos. Avatars are generated initials, product images are SVG icons on a coloured
tile (thumb() in section 11 of the script), and charts are SVG. The logo is
assets/img/logo.svg with the name "Northstar" in a paragraph link at the top of the sidebar (and
in the minimal header and the sign-in page); edit them in Appearance > Editor > Patterns. The
site name in browser tabs comes from Settings > General > Site Title. The social image is
assets/img/og-image.png (1200 × 630, source og-image.svg); the favicon assets/img/favicon.svg
is used until you set a Site Icon in Settings > General.
Sidebar, top bar and footer#
The app layout's parts are in Appearance > Editor > Patterns: Sidebar, Header (the top bar), Footer, and the minimal header and footer of the 404 page.
The sidebar menu is two List blocks of links (Workspace and Account). Each item's icon is a class
on the list item: icon-before-overview, icon-before-analytics, icon-before-orders,
icon-before-customers, icon-before-products, icon-before-settings, icon-before-help, with
icon-size-20 (the artwork is in src/sidebar-icons.css; add your own there and run
pnpm build). Each link has the class nav-item and its text a sb-label span (hidden when the
sidebar is collapsed). The script marks the link to the current page with aria-current="page". To
add a page, add a list item, and an entry to the SEARCH list (section 5 of the script) so global
search finds it.
The top bar is Custom HTML: the search box, the dark mode switch, the notifications menu and the account menu.
Dark mode#
A small script printed first in <head> (freeframe_dashboard_theme_script() in
inc/setup.php) applies the saved theme and sidebar state before the page paints. The switches
([data-theme-toggle]) toggle the dark class on <html> and save ns-theme in localStorage;
without a saved choice the visitor's system setting decides. To always show the light theme, remove
the switches from the Header, Header (minimal) and sign-in patterns and add this to a child theme's
functions.php:
add_filter( 'freeframe_dashboard_dark_mode', '__return_false' );Features#
Sidebar and mobile drawer#
On wide screens the sidebar can be collapsed to icons; the choice is saved as ns-sidebar.
Collapsed links keep accessible names (aria-label) and tooltips, and the collapse button is then
named "Expand sidebar" (with aria-expanded="false"). Below 1024 px the sidebar
becomes a drawer with a backdrop: focus is trapped inside, Escape or the backdrop closes it, and
focus returns to the menu button.
Global search#
Ctrl K / ⌘ K or / focuses the search box in the top bar. Results come from SEARCH (pages,
settings sections, every customer, order and product) and are navigable with the arrow keys and
Enter. Results link to filtered pages, such as /orders/?q=NS-4821 or
/customers/?q=Olivia%20Martin.
Menus, dialogs and toasts#
Dropdown menus (notifications, account) open on click and close with Escape or a click outside;
arrow keys move between items. Elements with data-modal are dialogs: data-modal-open="<id>"
opens one, data-modal-close closes it, focus is trapped and returned. Any element with
data-toast-msg="..." shows a short status message when clicked.
Charts#
Section 7 of the script contains the chart functions: sparkline(el), lineChart(el, cfg),
donut(el, segments, centerLabel), barChart(el, cfg), funnel(el, steps) and heatmap(el).
They redraw on resize and show tooltips on hover. Line charts are also focusable: the Left/Right
arrow keys move from point to point and show each value, and Escape hides it. The overview's range
buttons switch the revenue chart between 7 days, 30 days and 12 months; clicking a legend item on
the analytics page hides that line.
Orders table#
The orders page filters by status tab and search (also from ?q=), sorts by any column header
(aria-sort is updated), paginates 10 rows per page, and supports selecting rows with a
bulk-action bar ("Mark as paid", "Export", "Clear"). The export is a demo and only shows a message.
Customers and products#
The customers page filters by segment and search and opens a slide-over detail panel with recent
orders and a notes form. The products page filters by category and switches between grid and table
views; the choice is saved as ns-products-view. Stock shows as a meter with "In stock", "Low
stock" or "Out of stock".
Settings#
The tabs support arrow keys, Home and End, and update the URL hash, so /settings/#team and
/settings/#billing open those tabs. The invite dialog adds a member to the list, plan buttons
switch the current plan, notification switches are role="switch" buttons, and the bio field has a
240-character counter.
Back to top#
A button appears after scrolling about one screen down; it scrolls to the top (instantly with
reduced motion), moves keyboard focus to the main content and lifts above the toast. It is created
by assets/js/main.js; to remove it, delete the "Back to top" part of that file and the
.back-to-top styles in src/theme.css, then run pnpm build.
Block styles#
| Block | Styles |
|---|---|
| Button | Fill (default, the primary indigo), Secondary |
Forms#
Forms with data-validate are checked on submit: required fields, email format and minlength.
Errors appear under each field, linked with aria-describedby, and focus moves to the first
problem. A valid form then calls the handler named by data-on-valid (from the HANDLERS object
in the script) or shows the message in data-success.
| Form | Page | Behaviour |
|---|---|---|
Sign in (data-on-valid="signin") |
Sign in | email and password (8+ characters); HANDLERS.signin goes to the Overview |
Profile (data-success="Profile saved") |
Settings | name, email, bio and other profile fields; shows a message |
Invite (data-on-valid="invite") |
Settings dialog | email and role; HANDLERS.invite adds the member to the team list |
| Customer note | Customers panel | note text; shows a message |
Nothing is saved or sent. To connect your API, look for the comment // Demo only: nothing is sent. in bindForms() (section 6 of the script) and send the data before showing the success
state, giving each form an action in its Custom HTML. Replace HANDLERS.signin with a call to
your authentication provider. A dashboard should only be reachable after a real sign-in: protect it
on your server, for example by making the pages private, not in this front-end code.
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 |
These are the HTML version's keys. Each value is 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). Visitors can reset them by clearing site data.
SEO and sharing#
Page titles come from WordPress: the page title, a middle dot and the site name (for example "Orders · Northstar Admin"); the front page shows the site name and tagline (Settings > General). The theme doesn't output meta descriptions or Open Graph tags; an admin UI usually shouldn't be indexed at all: tick Settings > Reading > Discourage search engines or make the pages private.
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks
(sidebar, top bar, labelled navigation, main, footer), one h1 per page, visible focus rings, a
labelled dark mode switch with aria-pressed, a drawer and dialogs that trap focus, menus with
arrow keys, a search combobox with a listbox, accessible tabs and switches, charts readable with the
keyboard, sortable table headers with aria-sort, live status messages, form errors announced
through aria-describedby and aria-invalid, and reduced-motion support. The demo passes axe
(WCAG 2.1 A and AA) in light and dark mode.
When you edit: keep one Heading 1 per page, keep link text meaningful, and label any field you add
to a form (<label for="...">).
Building and deploying#
You only need this if you change the theme's files. The stylesheet assets/css/theme.css is
compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/,
templates/, inc/ and assets/js/:
pnpm install
pnpm build # compile assets/css/theme.css and assets/css/editor.css
pnpm watch # recompile assets/css/theme.css on every change (run pnpm build before you commit)Run pnpm build after adding a utility class to a pattern, template or the script's markup,
otherwise the class has no style. Classes you type in the editor's Additional CSS class(es)
field are only styled if the stylesheet already contains them.
To deploy, zip the theme folder (named freeframe-dashboard, without node_modules/) and upload
it in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.
Translations#
The theme is translation-ready with the text domain freeframe-dashboard. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's interface strings
(messages, counts, search entries, form errors) are translated in PHP
(freeframe_dashboard_script_strings() in inc/setup.php). The demo data (names, products,
statuses, chart labels) is data in assets/js/main.js. Create a translation with Loco Translate,
or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-dashboard wp-content/themes/freeframe-dashboard/languages/freeframe-dashboard.potSave freeframe-dashboard-de_DE.mo (for example) in wp-content/languages/themes/ or in the
theme's languages/ folder. Page content created by Dashboard setup is copied into your pages in
the site's language at that moment; translate it in the editor like any other content.
Child themes#
Use a child theme for changes you want to keep when Dashboard is updated. Create
wp-content/themes/dashboard-child/style.css:
/*
Theme Name: Dashboard Child
Template: freeframe-dashboard
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the
same name. To use your own copy of assets/js/main.js, dequeue the parent's script
(freeframe-dashboard) and enqueue yours in the child's functions.php. The parent's stylesheet
only contains the classes the parent uses: if your child theme uses new utility classes, compile a
stylesheet for the child (copy package.json and src/theme.css, point the @source lines at
the child's folders) and enqueue it.
Troubleshooting#
- The pages look unstyled or new classes do nothing. The stylesheet only contains classes it
found when it was built. Run
pnpm buildafter editing patterns, then reload. - "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Dashboard.
- A chart or table is empty. Its Custom HTML lost the
data-attribute or id the script looks for; re-insert the section from Patterns > Dashboard. - The Sign in page shows the sidebar. Choose the Full screen (sign-in) template in the page's settings.
- Search results or the sign-in redirect go to a missing page. Update
freeframe_dashboard_page_urls()ininc/setup.phpafter renaming pages. - Dashboard setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Dashboard pages.
- The theme can't be installed on an older WordPress. Dashboard uses the Icon block's icon collections from WordPress 7.1; update WordPress first.
License#
MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme
folder). Free for personal and commercial use. Inter and JetBrains Mono are licensed under the SIL
Open Font License 1.1 (OFL.txt in each folder of assets/fonts/).