Free Bootstrap 5 admin template, published to npm as @adminkit/core.
npm ciinstalls dependencies and the lefthook git hooksnpm run devserves the pages on http://localhost:8080 and reloads on change (npm startalso opens the browser)npm run buildbuilds everything intodist/;npm run previewserves that buildnpm run lintruns ESLint,prettier --check,scripts/check-icons.mjs, which fails unlesssrc/js/modules/icons.jsimports exactly the Lucide icons the templates use, andscripts/stats.mjs --check, which fails whensrc/data/stats.jsonor the README block is stalenpm run icons:listregeneratessrc/data/icons-lucide.json(every Lucide icon with its search tags) fromlucide-static; run it after upgradinglucidenpm run formatrunsprettier --writenpm run statsregeneratessrc/data/stats.jsonand the README block between<!-- stats:start -->and<!-- stats:end -->(see Counts below)npm run stats:sync -- --pro ../adminkit-procopies a PRO checkout's committedsrc/data/stats.jsontosrc/data/editions/pro.json, then runsnpm run stats. Run it whenever PRO's pages, plugins or icons change, and before every releasenpm run screenshots -- --pro ../adminkit-probuilds and serves a PRO checkout and writes the upgrade page screenshots topublic/img/screenshots/pro/;npm run screenshots -- --freewrites the README screenshots to.github/screenshots/. On demand only, never in the build; it needs Playwright's Chromium (npx playwright install chromium). Keep the PRO set under about 2 MB
vite.config.jsis the build: a Vite multi-page app withsrc/pagesas root.vite/holds the small in-repo plugins (Handlebars frontmatter and layouts, HTML beautifier).src/pages/*.htmlare Handlebars page templates with YAML frontmatter (layout,title,name,active,meta_description;labelnames a page without atitlein the stats, andcategory: dashboardmarks dashboards). String values may hold Handlebars expressions, rendered with the global data (meta_description: "... {{formatNumber stats.icons.lucide}} icons").src/layoutsandsrc/partialsare Handlebars partials, named by their path in the directory ({{> dashboard/line-chart }}issrc/partials/dashboard/line-chart.hbs);src/helpersare the Handlebars helpers andsrc/datathe global template data.src/jsandsrc/scssare the JavaScript and Sass sources, built tojs/app.jsandcss/app.css;js/icons-all.jsholds every Lucide icon and is loaded by the icons page only.src/js/modules/*.jswrap the third-party plugins (Chart.js 4, SimpleBar 6, jsVectorMap, Vanilla Calendar Pro, Lucide icons) and expose the globals the inline page scripts use. Keep those output names; the templates and the npm package (@adminkit/core,files: ["dist"]) reference them. The package'sexportsmap@adminkit/coretodist/js/app.js(typed bysrc/types/app.d.ts, copied todist/js/app.d.tsbyvite/declarations.js; update it when a global changes),@adminkit/core/csstodist/css/app.css(typed bysrc/types/app.d.css.ts, so TypeScript resolves the import),@adminkit/core/headtodist/js/head.js, and keep@adminkit/core/dist/*for every other file.vite/classic-script.jswraps the bundles so they do nothing where there is nowindow, as when a framework renders on the server.- Icons are Lucide: markup is
<i data-lucide="<canonical name>"></i>(canonical names from https://lucide.dev/icons/, not deprecated aliases such ashome), styled through.lucideinsrc/scss/3-components/_lucide.scss.js/app.jsrenders each icon as an<svg>inside its<i>, which then hasdisplay: contents, so component rules stylesvg/.lucideand never rely on the SVG being a direct child or sibling. A new icon must be imported and added toiconsinsrc/js/modules/icons.js, ornpm run lintfails.js/icons-all.jsis built fromlucide-static/icon-nodes.jsonso it shares no chunk withjs/app.js, which must stay one self-contained file for the npm package. - Color modes:
css/app.cssserves light and dark;data-bs-themeon<html>picks the mode, set before the first paint byjs/head.js(src/js/head.js, a blocking classic script in<head>, before the stylesheet; it must not import anything) and changed afterwards only throughsrc/js/modules/color-mode.js(adminkit_config_colorModein localStorage:light,dark,auto). Dark values are$*-darkvariables insrc/scss/1-variables/_dark.scss. A component color that follows the mode uses a custom property (var(--bs-gray-200),var(--ak-surface-bg),var(--bs-body-color);--bs-gray-*and--bs-secondary/--bs-light/--bs-darkturn around in dark mode, see3-components/_root.scss); where a component needs a different color in dark mode, add an@include color-mode(dark) { ... }block at the end of its partial. Check both modes. - Markup added later:
src/js/modules/mount.jssets up what the modules enhance (icons, the sidebar and its toggle,.js-simplebar, color mode controls, reveal and count-up) onDOMContentLoadedand, through oneMutationObserveron<html>, whenever it is added later, so single-page apps, Turbo and Livewire need no extra calls. It observes<html>because Turbo Drive and Astro's view transitions replace<body>. A module registers its markup withmount(selector, setup)instead of aDOMContentLoadedlistener;setupruns once per element and may return a cleanup for when the element is removed. Behaviour that can listen ondocument(clicks, Bootstrap events) does, instead of binding to elements. - Sidebar (
src/js/modules/sidebar.js):#sidebaris a Bootstrap.offcanvas-lg. Below lg the navbar toggle opens it as a sheet; from lg up it hides and shows it (.collapsed). AdminKit PRO shares this code path and switches to its compact layout instead. - Charts and maps:
window.theme(src/js/modules/theme.js) holds the colors of the current mode andadminkit:themechangeannounces changes. Inline page scripts create charts withadminkit.chartjs("#id", function(theme) { return config; })and maps withadminkit.vectorMap(function(theme) { return options; }); the function is called again with the new colors when the mode changes, so read colors from itsthemeargument, never fromwindow.themecaptured outside it. public/is copied verbatim intodist/(img/, including the PRO screenshots inimg/screenshots/pro/that the upgrade page shows). The README screenshots live in.github/screenshots/, outside the build.dist/is the build output and is gitignored. Vercel builds it withnpm run build.src/js/modules/vector-maps/is vendored jsvectormap data and is excluded from ESLint and Prettier.
- A script only one page runs sits at the end of that page in
src/pages/*.html. A widget's script sits in the widget's partial, after its markup (src/partials/charts/,src/partials/dashboard/), so every page that renders the widget gets it and no other page does. Both are wrapped in{{#push "scripts"}}...{{/push}}. {{#push "scripts"}}(src/helpers/stack.js) adds the block's output to thescriptsstack, and{{stack "scripts"}}at the end of<body>in both layouts prints it afterjs/app.js, in render order, which is document order: the layout renders the page before it reaches{{stack}}. The stack lives on@root, so it belongs to one page render. The icons page loadsjs/icons-all.jsin its push block, before the inline script that uses it.- What differs per page, such as a widget's title, is a partial parameter (
{{> dashboard/line-chart title="Monthly Visitors" size="sm" }}); a widget never checks which page it is on. - Every inline script runs its code inside
document.addEventListener("DOMContentLoaded", ...). Inline scripts run as soon as they are parsed, whilejs/app.jsis a module script, which is deferred:adminkitand the other globals exist once it has run, just beforeDOMContentLoaded. The one exception ismaps-google.html, whoseinitMapsis the callback of the Maps API script. - No page renders the same widget twice. A second copy would push its script again and needs its own element ids first.
- Every page, dashboard, plugin and icon count in
src/and the README comes fromsrc/data/stats.json, whichscripts/stats.mjsgenerates and which is committed so it shows in diffs and works without a build. Templates read it as{{stats.pages.count}},{{stats.icons.lucide}}and so on, and PRO's as{{editions.pro.pages.count}}; format numbers with{{formatNumber ...}}. Never type a count by hand. - Sources: the pages and their frontmatter,
src/data/plugins.json(name,package,url,licenseper third-party plugin; a dependency that is not a plugin goes innotPluginswith the reason),src/data/icons-lucide.jsonandsrc/data/appearance.json(the color modes, whichjs/modules/color-mode.jsimports, and the one sidebar color scheme, a constant here). A new dependency fails lint until it is in one of the two lists. src/data/comparison.jsonholds this edition's column of the AdminKit and AdminKit PRO comparison: per row anid, alabeland avalue, either a stat path ({ "stat": "pages.count" }) or a literal. PRO's repository has the same rows with PRO's values.npm run statsresolves them intostats.json→comparisonand adds PRO's column from the snapshot, matched byid; a row missing on either side fails. The upgrade page and the README table read it.- PRO's numbers come from the committed snapshot
src/data/editions/pro.json(the same shape as PRO'sstats.json), never from the PRO repository at build time. Refresh it withnpm run stats:sync -- --pro <PRO checkout>after PRO's pages, plugins or icons change and before every release, and commit it. - The build regenerates
stats.json(vite/stats.js) sodist/is never stale; commit the result.
- Tokens:
$ak-transition-duration-fast|base|slowand$ak-transition-easing-standard|emphasizedinsrc/scss/1-variables/_app.scss, used asvar(--ak-duration-*)andvar(--ak-ease-*). Write transitions with Bootstrap's@include transition(...)so reduced motion turns them off; an animation must not move anything underprefers-reduced-motion: reduce(a fade is fine). - Reveal (
js/modules/reveal.js,3-components/_reveal.scss): every top-level.cardinside.ak-reveal-cards(the layouts put it on<main>, which itself fades in once the page scripts have run) and every.ak-revealelement fades up when it scrolls into view. The hidden state only applies underhtml.js-reveal, set byjs/head.js; nothing else is needed on a new page. - Count-up (
js/modules/count-up.js):data-count-upon an element that holds only a number (with any prefix, suffix,,separators and decimals) and sits on its own line. The text stays the final value for screen readers and reduced motion. - Skeletons (
js/modules/skeleton.js,3-components/_skeleton.scss): for content JavaScript draws, putaria-busy="true"on the card body and{{> skeleton }}inside aposition-relativeelement that has the content's final height (a.chartor amin-height).adminkit.chartjs()andadminkit.vectorMap()clear it; for anything else calladminkit.ready(element). All of them fireadminkit:ready. - Clickable cards:
a.cardor a card with a.stretched-linklifts on hover and focus; nothing to add. - Layout shift must stay 0: reserve the final size of anything a script draws, give images
widthandheight, and keep<i data-lucide>icons sized through the rules in_lucide.scss(component rules that size an icon listi[data-lucide]next tosvg/.lucide). - These modules must stay importable into the single
js/app.jsbundle: no dynamic imports and nothing that splits a chunk.
- Target: WCAG 2.2 AA in light and dark mode, on every page including the sidebar and navbar: 4.5:1 for text, 3:1 for large text, icons and controls, no serious or critical axe violations.
- Check with
@axe-core/playwrighton the built pages in both modes. - A new color that text or a control is drawn in must pass: use
ak-readable($color, $background)(src/scss/1-variables/_functions.scss) and$theme-text-colors/$theme-text-colors-darkfor theme colors as text; fills with white text must reach 4.5:1 (ak-text-on()picks white or dark text). - Icon-only links and buttons get an
aria-label; links that act as buttons getrole="button"; card titles areh2.card-titleunder the page'sh1.
- Commits follow Conventional Commits; see
CONTRIBUTING.mdfor types, scopes and examples. lefthook enforces them. - A PR that changes behaviour adds a line under
UnreleasedinCHANGELOG.md. Add it, or ask before opening the PR. - Changelog lines are short, imperative and user-facing, like the released entries: "Add pricing page (/pages-pricing.html)", "Replace Feather icons with Lucide", "Upgrade to Chart.js v4", "Upgrade dependencies". One line per change a template user would notice. No internal tooling (lint, CI, hooks, scripts), no explanations, no implementation details. Notable dependency majors get "Upgrade to X vN"; everything else is "Upgrade dependencies".