This guide is for the people who build, customize, deploy, and maintain AdminLTE: architects integrating it into a platform, front-end engineers building from source, DevOps owners deploying the compiled assets, and contributors hacking on the project itself. It documents the source-tree architecture, the build pipeline, the internals of all seven JavaScript plugins, the SCSS variable system, the Astro-based documentation site, the release/deploy model, security posture, and concrete improvement opportunities.
The companion User Guide covers consuming AdminLTE (markup, plugins, theming) from an app developer's seat. This guide goes underneath that — how AdminLTE is constructed.
Note
AdminLTE is a distributable front-end template, not a runnable service. It has no server, no database, no API, and no runtime configuration. Its "deployment" is publishing static CSS/JS/asset files to a CDN or web root. Read "architecture" here as build-time and source-tree architecture, and "operations" as build, release, and asset-hosting operations.
Table of Contents
- Executive Summary
- System Architecture
- Repository Structure
- The Build Pipeline
- JavaScript Plugin Internals
- The SCSS Architecture
- The Documentation Site (Astro)
- Configuration Reference
- Build & Release Operations
- Deployment & Performance
- Security Analysis
- Code Quality & Conventions
- Testing & CI
- Potential Improvements
- Contributing
- Quick Reference
1. Executive Summary
| Attribute | Value |
|---|---|
| Project | AdminLTE — responsive admin dashboard template |
| Version | 4.0.0 (first stable release of the v4 line) |
| License | MIT |
| Maintainer | Colorlib (ColorlibHQ) |
| Foundation | Bootstrap 5.3.8, vanilla TypeScript (no jQuery) |
| Authoring languages | TypeScript (plugins), SCSS (styles), Astro/MDX (docs site) |
| Build tooling | Rollup (JS), Sass + PostCSS (CSS), Astro (docs), npm-run-all orchestration |
| Distributable | dist/css/adminlte*.css + dist/js/adminlte*.js (+ source maps, assets) |
| Runtime deps (peer) | Bootstrap, Popper, OverlayScrollbars, Bootstrap Icons |
| Node baseline | 18 LTS minimum; 22 LTS in CI |
| Browser target | Modern evergreen; no IE 11 |
What v4 is. A ground-up rewrite on Bootstrap 5.3 that drops jQuery entirely. The interactive layer is seven small TypeScript classes compiled to a single UMD bundle; the visual layer is a modular SCSS system layered on Bootstrap's CSS-custom-property theming; and the documentation/demo site is an Astro static build. The published artifact is tiny — the JS bundle gzips to roughly 5 KB and the CSS to roughly 40 KB.
Architectural philosophy. Three principles run through the codebase:
- Lean core, bring-your-own widgets. AdminLTE bundles no charts, date pickers, or data-table libraries. It documents recommended third-party libraries instead, keeping its own footprint minimal.
- Declarative data API over imperative wiring. Every plugin is driven by
data-lte-*attributes and self-initializes onDOMContentLoaded, so consumers rarely write JavaScript. - Defer to Bootstrap where Bootstrap is authoritative. Color modes use Bootstrap 5.3's
data-bs-theme; RTL uses logical properties; modal/dropdown focus and Escape handling are left to Bootstrap. AdminLTE adds only the application shell.
2. System Architecture
AdminLTE has two distinct build outputs from one source tree: the distributable template (what consumers install) and the documentation/demo site (what adminlte.io serves).
src/ (authored source)
│
┌─────────────────┼──────────────────────────┬─────────────────┐
│ │ │ │
src/ts/*.ts src/scss/*.scss src/assets/ src/html/ (Astro)
(TypeScript) (SCSS modules) (images) pages + components + docs
│ │ │ │
Rollup + Sass → PostCSS fs-extra copy Astro static build
@rollup/plugin- (autoprefixer, │ (reads dist/ via
typescript rtlcss) → clean-css │ copy-assets)
│ │ │ │
▼ ▼ ▼ ▼
dist/js/ dist/css/ dist/assets/ dist/html/ → flattened → dist/
adminlte.js adminlte.css (demo pages: index, docs, …)
adminlte.min.js adminlte.min.css
(+ .map) adminlte.rtl.css
adminlte.rtl.min.css
(+ .map)
Two consumption modes
- Prebuilt CSS/JS — consumers load
dist/css/adminlte.min.cssanddist/js/adminlte.min.js(via CDN, npm, or Composer) and never touch the build. This is the dominant mode. - Source build — consumers who need to change SCSS variables (sidebar width, breakpoints) point their bundler at
src/scss/adminlte.scssor clone and run the npm build.
The interactive layer (runtime)
At runtime in the browser, the architecture is event-driven and stateless beyond the DOM:
DOMContentLoaded
│
├─ Layout → manages body classes (hold-transition, app-loaded), resize handler
├─ PushMenu → reads sidebar data-attrs, sets up overlay, wires toggle buttons, resize handler
├─ Treeview → wires data-lte-toggle="treeview" click delegation
├─ CardWidget → wires card-collapse/remove/maximize buttons
├─ DirectChat → wires chat-pane buttons
├─ FullScreen → wires fullscreen buttons
└─ initAccessibility() → installs live region, skip links, key handlers, MutationObserver
There is no central registry or app object — each module independently queries the DOM and attaches listeners. State lives entirely in CSS classes on <body>, <html>, .card, and .nav-item, plus two optional localStorage keys (lte.sidebar.state, and the docs' theme key). This makes the bundle resilient to partial DOMs and framework re-renders, at the cost of having no single source of truth.
3. Repository Structure
AdminLTE/
├── src/
│ ├── ts/ # TypeScript plugin source (the JS product)
│ │ ├── adminlte.ts # bundle entry point — imports + re-exports all plugins
│ │ ├── layout.ts # Layout plugin
│ │ ├── push-menu.ts # PushMenu (sidebar) plugin
│ │ ├── treeview.ts # Treeview (sidebar submenu) plugin
│ │ ├── card-widget.ts # CardWidget plugin
│ │ ├── direct-chat.ts # DirectChat plugin
│ │ ├── fullscreen.ts # FullScreen plugin
│ │ ├── accessibility.ts # AccessibilityManager (WCAG 2.1 AA)
│ │ └── util/index.ts # shared utilities (onDOMContentLoaded, slideUp/Down, etc.)
│ ├── scss/ # SCSS source (the CSS product)
│ │ ├── adminlte.scss # master import file
│ │ ├── _variables.scss # AdminLTE variables (sidebar, header, transitions)
│ │ ├── _bootstrap-variables.scss # Bootstrap variable overrides
│ │ ├── _variables-dark.scss # dark-mode variable overrides
│ │ ├── _app-*.scss # layout regions (wrapper, header, sidebar, main, content, footer)
│ │ ├── _cards.scss, _small-box.scss, _info-box.scss, _timeline.scss, … # components
│ │ ├── mixins/ # _animations.scss, _scrollbar.scss
│ │ ├── pages/ # _lockscreen.scss, _login_and_register.scss
│ │ └── parts/ # _core, _components, _extra-components, _pages, _miscellaneous
│ ├── config/ # build configuration
│ │ ├── rollup.config.js # JS bundling (UMD, banner, TypeScript plugin)
│ │ ├── astro.config.mjs # docs-site build
│ │ ├── postcss.config.mjs # autoprefixer + rtlcss
│ │ └── assets.config.mjs # copies src/assets → dist/assets
│ ├── html/ # Astro documentation + demo site
│ │ ├── pages/ # .astro pages (index, docs/*, layout/*, pages/*, forms/*, …)
│ │ ├── components/ # .astro layout partials + .mdx doc content
│ │ ├── public/ # build output staged for Astro (copied from dist/)
│ │ └── env.d.ts, *.tsconfig.json
│ ├── utils/index.js # tiny helper (convertPathToHtml) used by the docs build
│ └── assets/ # source images (logos, avatars, demo photos)
├── dist/ # BUILD OUTPUT — committed to the repo (see §9)
├── package.json # scripts + devDependencies (no runtime deps)
├── composer.json # Packagist metadata (PHP distribution)
├── tsconfig.json # TypeScript compiler options (strict, ES2022, DOM)
├── eslint.config.js # flat ESLint config (typescript-eslint + unicorn + astro)
├── .stylelintrc.json # Stylelint (twbs-bootstrap config + overrides)
├── .browserslistrc # browser targets
├── .bundlewatch.config.json # per-asset size budgets enforced in CI
├── netlify.toml # docs-site deploy config (static publish of dist/)
└── README.md, SECURITY.md, CHANGELOG.md, ACCESSIBILITY-COMPLIANCE.md, LICENSE
Triage of importance
When reasoning about AdminLTE's behavior, the high-value source is:
| Folder | Role | Source weight |
|---|---|---|
src/ts/ |
All interactive behavior — the 7 plugins + utilities | ~49 KB, highest |
src/scss/ |
All styling — layout grid, components, theming | ~26 SCSS modules |
src/config/ |
The build wiring | 4 config files |
src/html/ |
Docs + demo pages (Astro/MDX) — also the project's own authored documentation | large but non-shipping |
dist/ |
Generated; do not edit by hand | committed artifact |
Important
The actual user-facing documentation prose lives in
src/html/components/docs/*.mdxandsrc/html/components/javascript/*.mdx. These MDX files are the canonical, hand-written reference for install paths, the migration guide, RTL, deployment, and each plugin's API — treat them as authoritative source, not as demo filler.
4. The Build Pipeline
The build is orchestrated entirely through package.json scripts using npm-run-all for parallel/sequential composition. There is no Gulp/Grunt — v4 replaced AdminLTE 3's Gulp pipeline with direct CLI invocations.
The full production build
npm run production runs: clean → lint → compile → bundlewatch.
clean rimraf dist
lint npm-run-all --parallel js-lint css-lint docs-lint lockfile-lint
compile css → js → assets → copy-assets → docs-compile → docs-format → flatten-build
bundlewatch enforce per-asset size budgets (.bundlewatch.config.json)
CSS pipeline (npm run css)
Four sequential stages: css-compile → css-prefix → css-rtl → css-minify.
| Stage | Tool | What it does |
|---|---|---|
css-compile |
sass |
Compiles src/scss/ → dist/css/ (expanded style, source maps, --load-path=node_modules) |
css-prefix |
postcss + autoprefixer |
Adds vendor prefixes in place (skips *.rtl.* and *.min.*) |
css-rtl |
postcss + rtlcss (NODE_ENV=RTL) |
Generates *.rtl.css from the LTR output |
css-minify |
clean-css-cli |
Produces *.min.css for both LTR and RTL, with source maps |
The RTL flip is driven by src/config/postcss.config.mjs, which conditionally enables rtlcss only when process.env.NODE_ENV === 'RTL':
plugins: {
...(process.env.NODE_ENV === 'RTL' ? { rtlcss: {} } : {}),
autoprefixer: { cascade: false }
}
JS pipeline (npm run js)
Two stages: js-compile → js-minify.
| Stage | Tool | What it does |
|---|---|---|
js-compile |
rollup (src/config/rollup.config.js) |
Bundles src/ts/adminlte.ts → dist/js/adminlte.js (UMD, source map) |
js-minify |
terser |
Produces dist/js/adminlte.min.js (2-pass compress, mangle, preserves /^!/ banner, source map) |
The Rollup config emits a UMD bundle named adminlte, stamps a copyright banner from package.json, and runs source through @rollup/plugin-typescript against the root tsconfig.json:
export default {
input: 'src/ts/adminlte.ts',
output: { file: 'dist/js/adminlte.js', format: 'umd', banner, name: 'adminlte' },
plugins: [ typescript({ tsconfig: './tsconfig.json', sourceMap: true }) ]
}
Assets and docs
| Script | Action |
|---|---|
assets |
node src/config/assets.config.mjs → fs-extra copies src/assets to dist/assets |
copy-assets |
Stages dist/css, dist/js, dist/assets into src/html/public/ so Astro can serve them |
docs-compile |
astro build (config in src/config/astro.config.mjs) → dist/html/ |
docs-format |
prettier --write "dist/**/*.html" |
flatten-build |
Moves dist/html/* up into dist/, removes the Astro intermediate, normalizes docs.html → docs/index.html |
Watch mode (npm run dev)
npm run dev runs watch and docs-serve in parallel via npm-run-all. watch uses concurrently to run four nodemon watchers:
| Watcher | Triggers |
|---|---|
watch-css-main |
src/scss/ .scss change → lint + compile + prefix + copy-assets |
watch-css-dist |
dist/css/ .css change → regenerate RTL |
watch-js-main |
src/ts/ .ts change → lint + compile + copy-assets |
watch-assets |
src/assets/ change → re-run assets copy |
docs-serve runs astro dev --open --port 3000.
Note
The dev server and docs share port 3000 (also configured in
astro.config.mjswithhost: '0.0.0.0'). The.gitpod.ymlopens a preview on port 3000 withnpm install && npm run dev.
5. JavaScript Plugin Internals
All plugins share a common shape: a constants block (DATA_KEY, EVENT_KEY, SELECTORs, CLASS_NAMEs), a class definition, and a onDOMContentLoaded(() => …) data-API block at the bottom that queries the DOM and wires listeners. They are imported and re-exported by src/ts/adminlte.ts:
import { onDOMContentLoaded } from './util/index.js'
import Layout from './layout.js'
import CardWidget from './card-widget.js'
import Treeview from './treeview.js'
import DirectChat from './direct-chat.js'
import FullScreen from './fullscreen.js'
import PushMenu from './push-menu.js'
import { initAccessibility } from './accessibility.js'
onDOMContentLoaded(() => {
const accessibilityManager = initAccessibility({ /* all features true */ })
accessibilityManager.addLandmarks()
})
export { Layout, CardWidget, Treeview, DirectChat, FullScreen, PushMenu, initAccessibility }
5.1 Shared utilities (util/index.ts)
The utility module is the foundation every plugin builds on:
| Export | Purpose |
|---|---|
onDOMContentLoaded(cb) |
Runs cb immediately if the DOM is ready, else queues it. Maintains a single shared callback array and registers one DOMContentLoaded listener — all plugins share the same listener. |
slideUp(target, duration=500) |
jQuery-free slide-up: animates height/margin/padding to 0 with inline transitions, then sets display:none and removes inline styles. |
slideDown(target, duration=500) |
Inverse — measures natural height, animates from 0. |
slideToggle(target, duration) |
Picks slideUp/slideDown by current display. |
hasDataAttribute(el, attr) |
Object.hasOwn(el.dataset, attr) (ES2022). |
getLastElement(elements) |
Array.from(...).at(-1) (ES2022). |
safePropertyAccess(obj, prop) |
Guarded property read; throws Error with { cause } on failure. |
Note
The single error string in the whole bundle comes from
safePropertyAccess:Failed to access property '<name>'(src/ts/util/index.ts). This is the only thrown error in the runtime — the plugins otherwise fail silently/defensively (null-checks,try/catcharoundlocalStorage).
The slideUp/slideDown helpers are the animation engine for CardWidget (collapse/remove) and Treeview (submenu open/close).
5.2 PushMenu (push-menu.ts)
The most complex plugin. Manages sidebar state across desktop/mobile with optional persistence.
Config (type Config): sidebarBreakpoint (default 992), enablePersistence (default false). Read from data-sidebar-breakpoint and data-enable-persistence on .app-sidebar.
State model — three body classes are the source of truth: sidebar-collapse, sidebar-open, sidebar-mini. Key methods:
expand()— removessidebar-collapse; on mobile addssidebar-open; firesopen.lte.push-menu.collapse()— removessidebar-open, addssidebar-collapse; firescollapse.lte.push-menu.isMobileSize()—window.innerWidth <= sidebarBreakpoint.setupSidebarBreakPoint()— reads the breakpoint from the::beforepseudo-elementcontentof the[class*="sidebar-expand"]element, so the JS breakpoint stays in sync with the CSS media queries.updateStateByResponsiveLogic()— on mobile, collapse unless explicitly open; on desktop, expand unless mini+collapsed.loadSidebarState()/saveSidebarState()/clearSidebarState()— localStorage underlte.sidebar.state, all wrapped intry/catchfor private-browsing/quota safety and SSR (globalThis.localStorage === undefined) guards.
Data-API block creates a .sidebar-overlay element appended to .app-wrapper, wires touch (touchstart/touchmove/touchend with a moved-guard) and click handlers to collapse on outside-tap, wires all [data-lte-toggle="sidebar"] buttons, and re-runs the responsive logic on window resize.
5.3 Treeview (treeview.ts)
Config: accordion (default true), animationSpeed (default 300). Read from data-accordion / data-animation-speed on the toggled element.
open() — if accordion, slides up sibling .nav-item.menu-open submenus first, then adds menu-open to this item and slideDowns its .nav-treeview; fires expanded.lte.treeview. close() removes menu-open and slideUps. The data-API block uses event delegation from the document, guards against non-menu clicks (only acts when a .nav-treeview child exists), and dispatches load.lte.treeview for pre-opened (menu-open) submenus on load.
5.4 CardWidget (card-widget.ts)
Config: animationSpeed (500), and three trigger selectors. The class resolves _parent as the closest .card (or the element itself if it is a card). Methods: collapse()/expand() (uses :scope > .card-body, :scope > .card-footer so nested cards are unaffected), remove(), maximize()/minimize() (sets fixed sizing, toggles maximized-card on both .card and <html>, tracks was-collapsed), and the toggle* variants. Each fires its corresponding *.lte.card-widget event.
5.5 DirectChat (direct-chat.ts)
The simplest plugin. toggle() flips direct-chat-contacts-open on the .direct-chat container and fires expanded/collapsed.lte.direct-chat. Constructor takes only the element — no config.
5.6 FullScreen (fullscreen.ts)
Wraps the browser Fullscreen API. inFullScreen() calls document.documentElement.requestFullscreen() and toggles .d-none on [data-lte-icon="maximize"]/[data-lte-icon="minimize"]; outFullscreen() calls document.exitFullscreen(). toggleFullScreen() checks document.fullscreenEnabled and document.fullscreenElement first.
Tip
The
.d-nonetoggle (rather than inlinedisplay) is deliberate — it fixed issue #6021 where the maximize icon shifted position. It keeps the icon library's naturaldisplayvalue (some icon fonts useinline-block), so it works with FontAwesome, Lucide, Tabler Icons, etc.
5.7 Layout (layout.ts)
holdTransition(time=100) adds hold-transition to <body> and removes it after time ms (clearing any prior timer). The data-API block instantiates Layout(document.body), holds transitions on window resize (200 ms), and adds app-loaded to <body> 400 ms after load.
5.8 AccessibilityManager (accessibility.ts)
The largest module. Constructed with a Partial<AccessibilityConfig> (all five flags default true) and runs an init() that conditionally installs each feature:
| Method | WCAG | Mechanism |
|---|---|---|
createLiveRegion() |
4.1.3 | Appends an aria-live="polite", role="status" div (#live-region) |
addSkipLinks() / ensureSkipTargets() |
2.4.1 | Inserts skip links; assigns id/tabindex to main/nav targets |
initFocusManagement() |
2.4.3/2.4.7 | Tab/Escape handlers; modal + dropdown focus on Bootstrap events |
initKeyboardNavigation() / handleMenuNavigation() |
2.1.1 | Arrow/Home/End nav inside .nav, .navbar-nav, .dropdown-menu |
respectReducedMotion() |
2.3.3 | On prefers-reduced-motion, adds reduce-motion + injects a duration-killing <style> |
initErrorAnnouncements() |
3.3.1 | A MutationObserver on document.body announces .alert-danger/.invalid-feedback (assertive) and .alert-success (polite) |
initTableAccessibility() |
1.3.1 | Adds role, scope on th, and a caption from title |
initFormAccessibility() / handleFormError() |
3.3.1/3.3.2 | Auto-aria-labels from placeholder, marks required, appends .invalid-feedback on invalid events |
Public API: announce(message, priority), focusElement(selector), trapFocus(container), addLandmarks(). Plus a standalone accessibilityUtils export with checkColorContrast, generateId, and isFocusable.
Important
addLandmarks()deliberately skips<ul>/<ol>elements when assigningrole="navigation". Assigning a navigation role to a list strips its implicit list semantics and orphans its<li>children for screen readers — this fixed issue #6038 (a Lighthouse "list items not contained within a list parent" failure). When auditing or extending this module, preserve that guard.
6. The SCSS Architecture
The stylesheet is a modular SCSS system imported through one master file, src/scss/adminlte.scss. It layers on top of Bootstrap rather than replacing it.
Layering model
_bootstrap-variables.scss ← Bootstrap variable overrides ($primary, breakpoints, spacing)
_variables.scss ← AdminLTE variables ($lte-sidebar-width, $lte-app-header-height, …)
_variables-dark.scss ← dark-mode variable overrides
↓ (all use !default — consumer overrides win if declared first)
Bootstrap import
↓
_app-wrapper / _app-header / _app-sidebar / _app-main / _app-content / _app-footer (layout grid)
↓
component partials (_cards, _small-box, _info-box, _timeline, _direct-chat, _callouts, _toasts,
_progress-bars, _dropdown, _table, _compact-mode, _accessibility, …)
↓
mixins/ (_animations, _scrollbar) and parts/ (_core, _components, _extra-components, _pages, _miscellaneous)
Key variables (defaults)
// Sidebar
$lte-sidebar-width: 250px; // mini state collapses to ~70px
$lte-sidebar-breakpoint: lg;
$lte-sidebar-padding-x: .5rem;
$lte-sidebar-padding-y: .5rem;
// Sidebar colours (light)
$lte-sidebar-color: $gray-800;
$lte-sidebar-hover-bg: rgba($black, .1);
$lte-sidebar-active-color: $black;
$lte-sidebar-menu-active-bg: rgba($black, .1);
// Header
$lte-app-header-height: $nav-link-height + ($lte-app-header-link-padding-y * 2);
$lte-app-header-bottom-border-color: var(--#{$prefix}border-color);
// Transitions
$lte-transition-speed: .3s;
$lte-transition-fn: ease-in-out;
Because all variables carry !default, downstream projects override them by declaring values before @import "node_modules/admin-lte/src/scss/adminlte".
The wrapper grid
.app-wrapper is a CSS-grid container; .app-header, .app-sidebar, .app-main, .app-footer are grid areas positioned by class regardless of source order. The responsive sidebar breakpoint is encoded in CSS via media queries on the [class*="sidebar-expand"] element, with the numeric value exposed through a ::before content value that PushMenu.setupSidebarBreakPoint() reads back — this is how the SCSS breakpoint and the JS breakpoint stay synchronized without duplication.
Stylelint constraints
.stylelintrc.json extends stylelint-config-twbs-bootstrap and adds notable disallow lists for .scss files:
function-disallowed-list:calc,lighten,darken(use CSScalc()sparingly / prefer relative color syntax)property-disallowed-list:border-radius(+ corners) andtransition(use the Bootstrap/AdminLTE variables and mixins instead of hardcoding)declaration-property-value-disallowed-list:border: none,outline: none(use0)
Note
These lint rules are why the codebase avoids raw
transition/border-radiusdeclarations — they enforce going through variables so theming stays consistent and RTL/dark mode keep working.
7. The Documentation Site (Astro)
src/html/ is a self-contained Astro project that builds the demo dashboard and the documentation into static HTML. It is not part of the distributable template — it's the project's own marketing/docs site, deployed separately (Netlify).
Configuration (src/config/astro.config.mjs)
export default defineConfig({
output: 'static',
build: { format: 'file' }, // page.html, not page/index.html
compressHTML: false,
markdown: { shikiConfig: { theme: 'dark-plus' } },
integrations: [mdx()],
srcDir: './src/html',
publicDir: './src/html/public', // staged dist/ assets live here
cacheDir: './dist/.astro',
outDir: './dist/html',
server: { host: '0.0.0.0', port: 3000 }
})
Structure
src/html/pages/*.astro— the routed pages: the three dashboards (index,index2,index3),docs/*(one page per doc topic),layout/*(the 11 layout demos),pages/*(apps: calendar, kanban, chat, file-manager, invoice, profile, settings, pricing, faq, 404, 500, maintenance),forms/*,tables/*,UI/*,widgets/*,examples/*(login/register/lockscreen).src/html/components/—.astrolayout partials (_head,_scripts, dashboard_topbar/_sidenav/_footer) and the.mdxdocumentation content (docs/*.mdx,javascript/*.mdx).@components/*is path-aliased tosrc/html/components/*intsconfig.json.
The build copies dist/css, dist/js, dist/assets into src/html/public/ (via copy-assets) so the demo pages reference the just-built template, then flatten-build lifts dist/html/* to the dist/ root for publishing.
Note
Because the docs site reads from
dist/, you must build the template (npm run css js assets copy-assets) before the docs site will render the current styles.npm run devwires this automatically viacopy-assetsin the watchers.
8. Configuration Reference
AdminLTE has no runtime configuration files — there is no app config, no environment variables consumed at runtime, no secrets. The repo's "configuration" is entirely build-time and tooling. The briefing's ground-truth scan correctly returned no application config files: a template has none.
Build & tooling config files
| File | Tool | Purpose |
|---|---|---|
tsconfig.json |
TypeScript | strict everything, target: es2022, lib: [es2022, DOM], module: esnext, moduleResolution: bundler, includes src/ts/**/*, @components/* path alias |
eslint.config.js |
ESLint (flat) | @eslint/js recommended + typescript-eslint + eslint-plugin-unicorn (flat/recommended) + eslint-plugin-astro; custom rule tweaks (comma-dangle: never, 2-space indent, semi: off) |
.stylelintrc.json |
Stylelint | twbs-bootstrap config + SCSS disallow lists (see §6) |
src/config/rollup.config.js |
Rollup | UMD bundle, banner, TS plugin |
src/config/astro.config.mjs |
Astro | Static docs build |
src/config/postcss.config.mjs |
PostCSS | autoprefixer + conditional rtlcss |
src/config/assets.config.mjs |
fs-extra | Copy assets |
.browserslistrc |
Browserslist | Target browsers (consumed by autoprefixer) |
.bundlewatch.config.json |
bundlewatch | Per-asset size budgets |
.prettierrc, .editorconfig, .gitattributes |
formatting | Code style |
netlify.toml |
Netlify | Docs-site deploy (static publish of dist/) |
TypeScript compiler options (notable)
{
"strict": true, "strictNullChecks": true, "strictPropertyInitialization": true,
"noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noImplicitAny": true,
"target": "es2022", "lib": ["es2022", "DOM"],
"module": "esnext", "moduleResolution": "bundler", "removeComments": true
}
The strictness here is meaningful: every plugin is written to satisfy strictNullChecks and strictPropertyInitialization, which is why you see pervasive optional chaining and as HTMLElement | undefined casts in the source.
Bundle size budgets (.bundlewatch.config.json)
| Asset | Max size |
|---|---|
dist/css/adminlte.css |
50 kB |
dist/css/adminlte.min.css |
46 kB |
dist/css/adminlte.rtl.css |
50 kB |
dist/css/adminlte.rtl.min.css |
46 kB |
dist/js/adminlte.js |
9 kB |
dist/js/adminlte.min.js |
5.5 kB |
npm run bundlewatch fails the build if any artifact crosses its budget — a hard guard against accidental bloat.
9. Build & Release Operations
Building locally
npm install # respects the package.json overrides block (npm 8.3+)
npm run production # clean + lint + compile + bundlewatch → dist/
Tip
npm run build(justclean + compile) is the faster path for development/testing — it skips linting and the bundlewatch gate.
The committed dist/
Unusually, dist/ is committed to the repository. The netlify.toml comment explains why:
[build]
publish = "dist"
command = "echo 'Static deploy of committed dist/'"
Warning
Because
dist/is committed and the Netlify production deploy simply publishes the committed files (it runs no build), thedist/artifacts can drift fromsrc/if a contributor edits source without rebuilding. Always runnpm run productionand commit the regenerateddist/as part of any change that touchessrc/ts/orsrc/scss/. The repo history shows explicit "rebuild dist" commits for this reason. The Netlify static-publish approach was chosen to avoidnpm installfailing on Netlify due to Node-version drift (the build pinsNODE_VERSION = "22"to match CI).
Linting before release
npm run lint # parallel: js-lint, css-lint, docs-lint, lockfile-lint
lockfile-lint enforces that every dependency in package-lock.json resolves over https: from the npm registry — a supply-chain guard against lockfile tampering.
Versioning & distribution channels
| Channel | Package | Notes |
|---|---|---|
| npm | admin-lte |
main: dist/js/adminlte.min.js, style/sass fields point at dist/src |
| Packagist (Composer) | almasaeed2010/adminlte |
Same source tree |
| jsDelivr CDN | mirrors npm | Versioned URLs (@4.0.0) |
Releases are cut from the master branch. The changelog lives on the GitHub releases page and CHANGELOG.md.
10. Deployment & Performance
AdminLTE deploys as static files. The choices below materially affect page weight and load time.
What to deploy
Ship dist/ plus your own HTML/server-rendered templates:
dist/
├── css/ adminlte.css, adminlte.min.css, adminlte.rtl.css, adminlte.rtl.min.css
├── js/ adminlte.js, adminlte.min.js
└── assets/ (demo images — replace with your own)
Do not ship src/, node_modules/, or the demo HTML when embedding AdminLTE into your own app.
Production checklist
- [ ] Serve the
.minvariants (adminlte.min.css,adminlte.min.js) — never the unminified dev builds. - [ ] Enable gzip/brotli for
text/css,application/javascript,text/html,image/svg+xmlat the server/CDN. - [ ] Set cache headers — short
max-age+?v=4.0.0query string, or fingerprinted filenames withmax-age=31536000, immutable. - [ ] Add Subresource Integrity (
integrity+crossorigin) hashes if loading from a public CDN. - [ ] Use
defer(notasync) on the AdminLTE script so it runs after Bootstrap/Popper are parsed. - [ ] Strip
*.map,**/html/,**/.astro/before uploading a demodist/verbatim.
Caution
Do not use
asynconadminlte.min.js. The bundle waits forDOMContentLoaded, butasynccould let it execute before Bootstrap and Popper finish parsing — listeners would attach to elements/APIs that don't exist yet.deferis the supported and recommended attribute.
Bundle weights (gzipped)
| Asset | Size |
|---|---|
adminlte.min.css |
~40 KB |
adminlte.rtl.min.css |
~40 KB |
adminlte.min.js |
~5 KB |
A full first-page payload (HTML + Bootstrap + AdminLTE + Bootstrap Icons + Popper + OverlayScrollbars) is around 180 KB gzipped from CDN.
Loading order (authoritative)
<head>
<!-- 1 --> Bootstrap Icons font CSS
<!-- 2 --> OverlayScrollbars CSS
<!-- 3 --> AdminLTE CSS (imports Bootstrap CSS)
</head>
<body>
…
<!-- 4 --> Popper
<!-- 5 --> Bootstrap JS
<!-- 6 --> OverlayScrollbars JS
<!-- 7 --> AdminLTE JS (must load AFTER Bootstrap)
</body>
Advanced
- Critical CSS — inline above-the-fold styles and
preloadthe rest ofadminlte.min.css(Critters/Penthouse). Usually overkill for behind-login dashboards. - Per-route CSS — core CSS is small, but page-specific integrations (charts, calendars, tables) add up; load them only where used. Code-split heavy integrations in SPAs.
- Service worker — AdminLTE is static files; precache them with Workbox by URL + revision.
- Cloudflare — default Browser Cache TTL is 4 hours; purge affected paths after a release or users see stale CSS/JS.
11. Security Analysis
AdminLTE is a UI template with no server, no auth, no data handling, and no secrets. Its security surface is therefore narrow — but there are real considerations.
The project's own security posture
SECURITY.md and the README codify a clear development/production separation:
Deploy only compiled production assets (dist/js/adminlte.min.js, dist/css/adminlte.min.css) plus your own files. Do not deploy node_modules/, the demo/example HTML (index.html, index2.html, index3.html, etc.), src/, or dev config.
Important
CVE-2021-36471 is disputed. It refers to demo pages being reachable when developers incorrectly deploy the example HTML files to production — not to a vulnerability in AdminLTE's code. v4 maintains a clear separation between demo and production assets. The mitigation is operational: never publish the demo HTML to a production web root.
Supply-chain controls
| Control | Mechanism |
|---|---|
| Lockfile integrity | lockfile-lint enforces https: npm-registry resolution for every dependency |
| Dependency overrides | package.json overrides pins yaml and the stylelint config peer so npm install runs clean with 0 reported vulnerabilities (no --legacy-peer-deps) |
| CDN integrity | Docs recommend SRI hashes for all CDN-loaded assets (AdminLTE, Bootstrap, Popper, OverlayScrollbars, Bootstrap Icons) |
| Source maps | Documented as a leak vector — strip *.map from production uploads |
XSS / client-side considerations for integrators
Because AdminLTE is markup you fill with your own data, the XSS surface is yours, not AdminLTE's. Notable points:
- AdminLTE's plugins set DOM attributes and classes; they do not inject user-controlled HTML. The one DOM-string injection in the accessibility module (
announce) writes totextContent, notinnerHTML, so it is XSS-safe. - ESLint's
astro/no-set-html-directiveis turned off in this repo (the docs site usesset:htmlfor trusted MDX). When you adopt patterns from the demo, do not carry untrusted data intoinnerHTML/set:html. - The recipe patterns that
fetch()HTML into a card (body.innerHTML = html) assume trusted server responses — sanitize or switch totextContentif the source isn't yours.
Accessibility as a quality/compliance control
The bundled AccessibilityManager provides WCAG 2.1 AA features automatically (see §5.8 and ACCESSIBILITY-COMPLIANCE.md). For regulated deployments (Section 508, EN 301 549), this is a meaningful baseline — but verify your own content (color contrast of custom themes, alt text, form labels) since AdminLTE can only harden the shell.
12. Code Quality & Conventions
Language & style
- TypeScript, strict mode, ES2022 target. Pervasive null-safety (
?.,as T | undefined),Object.hasOwn,Array.at(-1),globalThis.*. - Plugin idiom is uniform: constants block (DATA_KEY / EVENT_KEY / SELECTOR_* / CLASS_NAME_*) → class →
onDOMContentLoadeddata-API block →export default. Learning one plugin means you can read all seven. - Event naming convention:
<verb>.lte.<plugin-key>(e.g.collapsed.lte.card-widget). DATA_KEY islte.<plugin>. - No jQuery, no framework. All DOM work is native (
querySelector,addEventListener,classList,dataset). - Defensive by default: localStorage access, fullscreen calls, and DOM lookups are all guarded; the bundle degrades rather than throws.
Lint enforcement
- ESLint (flat config):
typescript-eslintrecommended +unicornflat/recommended +astro. Custom:comma-dangle: never, 2-space indent,multiline-ternary: always-multiline,operator-linebreak: after, manyunicorn/*rules selectively disabled (e.g.prefer-query-selectoroff,prevent-abbreviationsoff). - Stylelint (twbs-bootstrap) with the disallow lists in §6.
- Linters run in parallel in
npm run lint, and as thelintstage ofnpm run production.
Conventions worth preserving when contributing
| Convention | Why |
|---|---|
Read config from data-*, fall back to a Default/Defaults object |
Keeps the declarative data API working without JS |
Animate via slideUp/slideDown from util/ |
One animation engine; consistent timing |
Guard localStorage and browser APIs |
SSR + private-browsing + quota safety |
Use .d-none / class toggles, not inline display/style |
Survives icon-library and theme differences (see #6021) |
Don't put role="navigation" on lists |
Preserves list semantics (see #6038) |
13. Testing & CI
AdminLTE 4 ships no unit-test suite — there is no tests/ directory, no Jest/Vitest config, and no test runner in package.json. Quality is enforced through a combination of static analysis and size gates rather than behavioral tests:
| Gate | Tool | Catches |
|---|---|---|
| Type checking | tsc (via Rollup's TS plugin, strict) |
Type errors, null-safety violations |
| JS lint | ESLint | Style + a class of bugs (unused vars, unsafe patterns) |
| CSS lint | Stylelint | Disallowed properties/functions, inconsistencies |
| Docs lint | astro check (docs-lint) |
Astro/TS errors in the docs site |
| Lockfile lint | lockfile-lint |
Supply-chain tampering |
| Size budget | bundlewatch |
Bundle bloat |
CI runs on Node 22 (per netlify.toml and the README's "Node 22 LTS in CI" note). The GitHub Actions workflows live under .github/workflows/ (referenced by netlify.toml, not included in the analyzed source set).
Warning
The absence of behavioral tests means regressions in plugin logic (e.g. the sidebar state machine, card maximize/restore, treeview accordion) can only be caught by manual demo-page testing. This is the single largest quality gap — see Improvements.
Manual verification surface
Because the demo site exercises every plugin and layout, the practical "test suite" is the demo pages: layout/* for the 11 layout modes, pages/* and widgets/* for components, and the dashboards (index, index2, index3) for the integrated experience. After any src/ts/ or src/scss/ change, run npm run dev and click through the affected demos.
14. Potential Improvements
Observations drawn from the source. None are blockers; they're the natural next steps for a maturing v4.
-
Add a behavioral test suite. The plugins are small, pure-ish DOM classes — ideal for Vitest + jsdom or Playwright component tests. Priority targets:
PushMenustate machine (mobile/desktop/persistence transitions),CardWidgetmaximize↔minimize withwas-collapsedtracking,Treeviewaccordion sibling-closing, and theAccessibilityManagerMutationObserver. This is the highest-leverage improvement given the committed-dist/model where a logic regression ships silently. -
Automate
dist/regeneration in CI. Sincedist/is committed and Netlify publishes it verbatim, a CI check that rebuilds and diffsdist/against the committed files would catch source/dist drift before merge. -
Per-plugin entry points for tree-shaking. The bundle is a single UMD file exporting all seven plugins. Consumers who need only
PushMenustill ship the whole bundle unless they import fromsrc/ts/(which requires TypeScript). Publishing per-plugin ESM entry points would let bundlers drop unused plugins. -
Document the SCSS variable surface exhaustively. The customization docs list the most-overridden variables, but
_variables.scsshas many more. A generated variable reference (name, default, what it controls) would reduce guesswork for theme authors. -
Consolidate the dark-mode storage key story. The FAQ references an
lte-themelocalStorage key while the color-mode toggle snippet usestheme. Aligning the docs (and ideally shipping the toggle as a tiny optional module with a documented key) would remove a real source of confusion. -
Refactor
convertPathToHtml(src/utils/index.js). It's a hardcodedcount === 2..5ladder for relative-path depth used by the docs build. A small loop ('../'.repeat(count - 2)style) would handle arbitrary depth and remove the magic numbers. -
Tighten the
safePropertyAccesscontract. It's the only thrower in the runtime and currently appears unused by the plugins. Either wire it where dynamic property access happens or drop it to keep the util surface minimal. -
Surface plugin config more uniformly.
CardWidgetreads nodata-*config (only programmatic), whilePushMenu/Treeviewread several. GivingCardWidgetadata-animation-speedattribute would make the declarative API consistent across plugins.
15. Contributing
The contribution flow (from the README):
- Install Node.js LTS (18+, 22 recommended).
- Clone the repo and switch to the
masterbranch. npm install(installs frompackage.json; theoverridesblock keeps peers clean).npm start(alias fornpm run dev) to launch the dev server athttp://localhost:3000.- Make changes in
src/—src/ts/for behavior,src/scss/for styles,src/html/for docs/demos. - Rebuild and commit
dist/if you changedsrc/ts/orsrc/scss/(npm run production). - Run
npm run lintand fix any failures. - Send a PR against
master.
Tip
Pre-opened submenus (
menu-open), the demo dashboards, andlayout/*pages are your fastest manual regression check. The CI gates (lint, type-check, bundlewatch) will catch the rest, but they won't catch behavioral regressions — verify interactive changes by hand.
Discussion happens in GitHub Discussions; bugs and feature requests go to Issues. There's also a Discord.
16. Quick Reference
Plugin class summary
| Class | File | Constructor | Key config defaults |
|---|---|---|---|
Layout |
layout.ts |
(element) |
— (auto on <body>) |
PushMenu |
push-menu.ts |
(element, config) |
sidebarBreakpoint: 992, enablePersistence: false |
Treeview |
treeview.ts |
(element, config) |
accordion: true, animationSpeed: 300 |
CardWidget |
card-widget.ts |
(element, config) |
animationSpeed: 500 |
DirectChat |
direct-chat.ts |
(element) |
— |
FullScreen |
fullscreen.ts |
(element) |
— |
AccessibilityManager |
accessibility.ts |
(config) via initAccessibility() |
all five features true |
localStorage keys
| Key | Owner | Values |
|---|---|---|
lte.sidebar.state |
PushMenu (when enablePersistence) |
sidebar-open / sidebar-collapse |
theme |
Docs color-mode snippet (not in bundle) | light / dark / auto |
Build commands
| Command | Purpose |
|---|---|
npm install |
Install deps (npm 8.3+ for overrides) |
npm run dev / npm start |
Dev server + docs at :3000 |
npm run build |
Fast build (clean + compile) |
npm run production |
Full build (clean + lint + compile + bundlewatch) |
npm run css / npm run js |
Build one half |
npm run lint |
All linters |
npm run bundlewatch |
Size-budget check |
Distributable artifacts
| File | Use |
|---|---|
dist/css/adminlte.min.css |
Production stylesheet (LTR) |
dist/css/adminlte.rtl.min.css |
Production stylesheet (RTL) |
dist/js/adminlte.min.js |
Production script (UMD, exports adminlte) |
dist/assets/ |
Demo images (replace with your own) |
Runtime peer dependencies
| Dependency | Pinned | Role |
|---|---|---|
| Bootstrap | 5.3.8 | Component framework + CSS-variable theming |
| Popper | 2.11.8 | Bootstrap overlays |
| Bootstrap Icons | 1.13.1 | Default icon font |
| OverlayScrollbars | 2.11.0 | Sidebar scrollbar |
Key facts (verified)
- Version 4.0.0, MIT, no jQuery, vanilla TypeScript.
- 7 plugins, single UMD bundle (
adminlte), ~5 KB gzipped JS / ~40 KB gzipped CSS. - No runtime config, no server, no tests directory. Quality via lint + type-check + bundlewatch.
dist/is committed; Netlify publishes it verbatim. Rebuild on everysrc/change.- TypeScript target ES2022, strict; Node 18+ (22 in CI); no IE 11.