NexaAdmin documentation
How to make pages, set colours and themes, and use the tables, charts, forms and overlays.
Quick start
- Unzip the download and open
index.htmlin your browser. Every page works straight from the folder. - Browse the 16 pages from the sidebar.
components.htmlshows every interface part on one page. - Copy
blank.htmlto start a page of your own. - Set your brand colour in one line (see Colours and themes).
There is no build step. The kit is plain HTML, one CSS file and three small JavaScript files. The only library is Chart.js, which is included in the download.
Files
NexaAdmin/
index.html dashboard
analytics.html visitors, pages, devices, countries
ecommerce.html products, sales, categories
orders.html data table: search, filter, sort, pages, bulk actions, CSV
users.html team cards, users table, invite form
calendar.html month calendar with events
messages.html inbox and conversation
invoice.html printable invoice
profile.html user profile
settings.html profile, security, notifications, appearance, billing
components.html every interface part
blank.html empty starter page
login.html register.html forgot-password.html 404.html
documentation.html this guide
assets/
css/nexa.css all styles
js/nexa.js theme, sidebar, menus, modals, toasts, tabs, forms, tables
js/charts.js the charts
js/pages.js sample logic for orders, users, products, calendar, messages
vendor/chart.umd.min.js Chart.js 4 (MIT license, see chart.LICENSE.md)
Making a page
Copy blank.html and rename it. Put your content inside <main class="content">. Each direct child of content is one row with even spacing.
<main class="content" id="main">
<section class="card">
<div class="card-head"><div><h2>Title</h2><p>Short description</p></div></div>
<div class="card-body">Your content</div>
</section>
</main>
Add it to the sidebar
The sidebar is repeated in every page file. Add your link in each one. The page that is open carries aria-current="page", which also highlights it.
<a class="side-link" href="reports.html" aria-current="page" title="Reports">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>
<span>Reports</span>
</a>
If your project has a template language (PHP includes, Blade, Twig, Django, EJS, React), move the sidebar and top bar into one shared partial so you only edit them once.
Page title and breadcrumb
Change the <h1> and the crumbs list in the top bar, and the <title> in the head.
Icons
Icons are inline SVG with the class icon. They take the text colour of their parent. Copy one from any page, or paste a 24 by 24 stroke icon from a free set such as Lucide or Feather.
Colours and themes
Brand colour
Open assets/css/nexa.css and change one line at the top. Buttons, active links, charts, focus rings and soft tints all follow.
:root {
--brand: #5b54e8; /* your colour */
--brand-ink: #ffffff; /* text on top of it: use #111 for a light brand colour */
}
Light and dark
The theme is the data-theme attribute on <html>: light or dark. The sun and moon button in the top bar switches it. The choice is saved in the browser and applied on every page before it paints, so there is no flash.
| What you want | How |
|---|---|
| Dark by default | Add data-theme-default="dark" to the <html> tag of each page |
| Follow the visitor's device | data-theme-default="auto" |
| A button that sets a theme | <button data-theme-set="dark">, with light, dark or auto |
| A button that flips it | <button data-theme-toggle> |
| From your own script | Nexa.setTheme('dark') |
Accent picker
The swatches on the settings page use data-accent="#0e8f7e". A click saves that colour in the browser and applies it everywhere. Remove the swatches if you want one fixed brand colour.
All tokens
Every colour is a CSS variable in the first section of nexa.css, with a second set under [data-theme="dark"]. The components page shows them as swatches.
| Variable | Used for |
|---|---|
--bg, --surface, --surface-2 | Page background, cards, and quiet areas such as table headers |
--line, --line-strong | Borders |
--ink, --ink-2, --ink-3 | Main text, secondary text, small notes |
--ok, --warn, --bad, --info | Status colours, each with a -soft background partner |
--side-bg | Sidebar background |
--sidebar-w, --topbar-h, --r-sm, --r-md, --r-lg | Sizes and corner radius |
--font | Font family. The kit loads Onest from Google Fonts |
Layout and grid
Wrap cards in a grid with one of these column classes. They collapse on smaller screens by themselves.
| Class | Desktop | Tablet | Phone |
|---|---|---|---|
grid cols-4 | 4 columns | 2 | 1 |
grid cols-3 | 3 | 2 | 1 |
grid cols-2 | 2 | 2 | 1 |
grid cols-2-1 | wide and narrow | 1 | 1 |
grid cols-1-2 | narrow and wide | 1 | 1 |
Helpers: row lines items up with a gap, row-end pushes an item to the right, stack spaces items vertically, muted is quiet text, num gives numbers equal widths, hide-xs hides on small phones.
Sidebar
Below 992 pixels the sidebar becomes a drawer opened by the menu button. On larger screens the button at the top left collapses it to icons, and the choice is saved. To start collapsed, add class="side-collapsed" to <html>.
Interface parts
Open components.html next to this guide. Each block there names its classes.
| Part | Classes |
|---|---|
| Card | card with card-head, card-body, card-foot |
| Stat card | card stat with stat-label, stat-icon tone-brand, stat-value, stat-foot |
| Button | btn, plus btn-primary, btn-danger, btn-ghost, btn-sm, btn-lg, btn-block. Square: icon-btn |
| Badge | badge, plus ok, warn, bad, info, brand. Add plain to hide the dot |
| Alert | alert, plus ok, warn, bad |
| Avatar | avatar, plus sm, lg, xl. Colour with style="--c:#12a594". Put an <img> inside for a photo |
| Progress | progress with style="--v:72%" and an empty <i> inside |
| Tabs | tabs with role="tablist", buttons with role="tab" and aria-controls |
| Segmented buttons | seg with data-seg="name". Fires a nexa:seg event with the chosen value |
| Lists | list, timeline, legend |
| States | empty, skeleton |
Tone classes colour an icon tile or a calendar event: tone-brand, tone-ok, tone-warn, tone-bad, tone-info.
Modals, menus, toasts
These are switched on with attributes. You do not need to write JavaScript for them.
Modal
<button class="btn" data-modal-open="myModal">Open</button>
<div class="modal" id="myModal" role="dialog" aria-modal="true" aria-labelledby="myTitle" aria-hidden="true">
<div class="modal-box">
<div class="modal-head"><h2 id="myTitle">Title</h2></div>
<div class="modal-body">...</div>
<div class="modal-foot"><button class="btn" data-modal-close>Close</button></div>
</div>
</div>
Focus moves into the modal and stays there. Escape, a click on the dark area or any data-modal-close button closes it and returns focus to the button that opened it. From a script: Nexa.openModal('myModal') and Nexa.closeModal().
Dropdown menu
<div class="dropdown">
<button class="btn" data-dropdown aria-expanded="false" aria-haspopup="true">Menu</button>
<div class="menu">
<a href="#">Edit</a>
<hr>
<a class="danger" href="#">Delete</a>
</div>
</div>
Toast
<button class="btn" data-toast="Saved">Save</button>
<button class="btn" data-toast="Could not save" data-toast-type="bad">Fail</button>
Nexa.toast('Saved'); // from a script
Nexa.toast('Could not save', 'bad');
Forms
Put each control in a field with a label and an empty error element.
<form data-validate>
<div class="field">
<label for="email">Email</label>
<input class="input" id="email" name="email" type="email" required>
<span class="error"></span>
</div>
<button class="btn btn-primary" type="submit">Save</button>
</form>
data-validate checks the form before it is sent and shows a message under each field that fails.
| Rule | How to set it |
|---|---|
| Must be filled in | required. Custom message: data-required="Please accept the terms." |
| Valid email | type="email" |
| Minimum length | minlength="8" |
| Must equal another field | data-match="idOfTheOtherField" |
Demo mode
The forms in the kit also carry data-demo. In demo mode a valid form is not sent anywhere. It shows the data-success message as a toast, closes its modal, and goes to data-redirect if one is set.
To send a form to your server, remove data-demo and add action and method. The checks still run first.
<form data-validate action="/login" method="post">
The sign in, register and reset pages are interface only. Real sign-in needs a server. Never keep passwords in the page.
A peek button inside input-wrap shows and hides a password. A switch is a checkbox styled as a toggle.
Data tables
Add data-table and an id to a normal table. The rows stay in the HTML, so your server can print them as usual.
<input type="search" data-table-search="#orders">
<select data-table-filter="#orders" data-key="status">
<option value="">All</option><option>Paid</option>
</select>
<table class="table" id="orders" data-table data-page-size="8">
<thead><tr>
<th data-sort="text">Customer</th>
<th data-sort="date">Date</th>
<th data-sort="num">Amount</th>
</tr></thead>
<tbody>
<tr data-status="Paid">
<td>Sara Lin</td>
<td data-value="2026-05-27">May 27, 2026</td>
<td data-value="299">$299.00</td>
</tr>
</tbody>
</table>
<span data-table-info="#orders"></span>
<nav class="pager" data-table-pager="#orders"></nav>
| Attribute | What it does |
|---|---|
data-sort on a th | Makes the column sortable as text, num or date |
data-value on a td | The value to sort by when the visible text is formatted |
data-table-filter with data-key="status" | Keeps rows whose data-status equals the chosen option |
data-table-bulk="#orders" | A bar shown while rows are ticked. Put <span data-count> inside for the number |
data-table-empty="#orders" | An element shown when no row matches |
data-table-export="#orders" | A button that downloads the matching rows as CSV. Columns marked data-no-export are left out |
From a script
var t = Nexa.tables.orders; // by table id
t.add(rowElement, true); // true puts it at the top
t.remove(t.selected()); // delete the ticked rows
t.render(); // after you change rows yourself
t.download('orders.csv');
The table works on the rows that are in the page. For very large data sets, page and search on your server and print one page of rows at a time.
Charts
Charts are drawn with Chart.js 4, included in assets/vendor. All of them are set up in assets/js/charts.js. A page with charts loads three scripts in this order:
<script src="assets/js/nexa.js"></script>
<script src="assets/vendor/chart.umd.min.js"></script>
<script src="assets/js/charts.js"></script>
Put the canvas in a chart-box with a height:
<div class="chart-box" style="height:300px">
<canvas id="signupsChart" role="img" aria-label="Line chart of sign-ups per month"></canvas>
</div>
Describe the chart once with Nexa.chart. It receives the current theme colours and is redrawn when the theme or accent changes.
Nexa.chart('signupsChart', function (c) {
return {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr'],
datasets: [{ label: 'Sign-ups', data: [120, 180, 150, 240], borderColor: c.brand, tension: 0.4 }]
},
options: { scales: { y: { grid: { color: c.line } } } }
};
});
c holds brand, ink, ink2, ink3, line, surface and series, a list of six colours for several data sets.
The numbers in charts.js are samples. Replace the arrays with your own data.
Page scripts
assets/js/pages.js holds the sample behaviour for five pages. Each block starts with a comment and only runs on its own page. Data is kept in memory, so it resets when the page reloads.
| Page | What the script does | Where to connect your data |
|---|---|---|
| Orders, users | Adds a row from the modal form, deletes one row or the ticked rows, marks orders completed | The nexa:submit handlers and the #confirmDelete click |
| E-commerce | Filters the product grid by the data-state of each card | Print your products as cards |
| Calendar | Draws the month, lists the day, adds and deletes events | The events array |
| Messages | Shows conversations, opens a thread, sends a message, searches people | The threads array and the form submit handler |
Everything typed by a user is escaped with Nexa.escapeHtml before it is put in the page. Keep doing that when you add your own fields.
Connecting your data
NexaAdmin is the interface. It does not include a server, a database or sign-in. Two common ways to use it:
- Server-rendered apps (PHP, Laravel, Rails, Django, Express): turn each page into a template, print your rows and numbers into the HTML, and keep the scripts as they are.
- API-driven pages: fetch JSON, build the rows or chart data, then call
table.render()orNexa.chart(...).
fetch('/api/orders')
.then(function (r) { return r.json(); })
.then(function (orders) {
var body = document.querySelector('#ordersTable tbody');
body.innerHTML = orders.map(function (o) {
return '<tr data-status="' + Nexa.escapeHtml(o.status) + '"><td>' + Nexa.escapeHtml(o.customer) + '</td>...</tr>';
}).join('');
Nexa.tables.ordersTable.render();
});
Browser support
Current versions of Chrome, Edge, Firefox and Safari, on desktop and mobile. The soft colour tints use CSS color-mix(), available in Chrome and Edge 111, Firefox 113, Safari 16.2 and newer.
Every page was checked at 320, 375, 768, 1024 and 1366 pixels wide in both themes. Motion is reduced for visitors who ask their device for it, and the invoice has a print layout.
Help and license
For help, reply to your purchase receipt email and say which page you are working on.
You may use the kit in unlimited projects for yourself and for clients. You may not resell or redistribute the kit itself. The full terms are in LICENSE.txt. Chart.js is included under its own MIT license.