Component development guide
Table of contents
- What “component” means in v4
- Recipe — pure CSS component
- Recipe — CSS + light JS
- Recipe — dedicated module (heavy library or stateful)
- Anti-patterns
- When to break the conventions
This guide is for people adding new UI components to Adminator 4 — not for using existing ones (that’s covered in Components reference).
The v3 era of “register a component class with AdminatorApp” is over. v4 components are CSS classes plus, optionally, a small init function in init.js.
What “component” means in v4
There are three possible levels of component:
-
Pure CSS — A class (or set) in one of the SCSS partials. No JS. Render-time behavior only. Most components fall here: buttons, cards, alerts, badges, progress bars.
-
CSS + light JS — A class plus a behavior in
init.js. The behavior is wired by attribute ([data-X]) so it self-discovers on every page. Examples: tab groups, accordions, dropdowns. -
CSS + dedicated JS module — A new module file under
src/assets/scripts/2026/. Used when the component wraps an external library or has its own non-trivial state. Current examples:charts.js,calendar.js,maps.js.
Pick the lowest level that meets your needs. Levels 1 and 2 are appropriate for 95% of cases.
Recipe — pure CSS component
Goal: add a “stat-tile” — a small inline KPI display.
1. Decide where the styles live
Pick the right partial:
| Partial | When to use |
|---|---|
_components.scss | Generic primitive used across pages |
_ui.scss | UI primitive (alert/badge/progress/spinner family) |
_forms.scss | Anything form-input-shaped |
_dashboard.scss | Dashboard-specific |
_email.scss / _calendar.scss / _chat.scss | Page-specific |
For a generic stat tile usable anywhere, that’s _components.scss.
2. Write the styles
/* in _components.scss */
.stat-tile {
display: flex;
align-items: baseline;
gap: 8px;
padding: 14px 16px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
}
.stat-tile-label {
font-size: 11px;
color: var(--t-light);
font-family: 'JetBrains Mono', monospace;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.stat-tile-value {
font-family: 'Inter Tight', sans-serif;
font-weight: 700;
font-size: 18px;
color: var(--t-base);
letter-spacing: -0.02em;
}
.stat-tile-delta {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--t-muted);
margin-left: auto;
}
.stat-tile-delta.up { color: var(--success); }
.stat-tile-delta.down { color: var(--danger); }
3. Use it
<div class="stat-tile">
<span class="stat-tile-label">MRR</span>
<span class="stat-tile-value">$84.2K</span>
<span class="stat-tile-delta up">+12%</span>
</div>
4. Done
That’s the whole component. Save the file, the dev server rebuilds. The tile works in light + dark mode automatically because every color is var(--…).
Recipe — CSS + light JS
Goal: add a “show password” eye toggle on password inputs.
1. Decide on a data attribute
Use a data-X attribute as the marker. Convention: data-show-password on the password input.
<input class="input" type="password" data-show-password>
2. Add the toggle behavior
In init.js, add an init function and call it from initShellBehaviors():
// inside init.js
function initShowPassword() {
document.querySelectorAll('input[data-show-password]').forEach((input) => {
// Wrap the input in a relative container with a button
const wrap = document.createElement('div');
wrap.style.position = 'relative';
input.parentNode.insertBefore(wrap, input);
wrap.appendChild(input);
const btn = document.createElement('button');
btn.type = 'button';
btn.setAttribute('aria-label', 'Show password');
btn.style.cssText = 'position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:0;color:var(--t-muted);cursor:pointer;';
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
wrap.appendChild(btn);
btn.addEventListener('click', () => {
input.type = input.type === 'password' ? 'text' : 'password';
});
});
}
export function initShellBehaviors() {
initThemeToggle();
initHeroDate();
initNavGroups();
initDropdowns();
initTodos();
initAccordions();
initTabGroups();
initShowPassword(); // ← add this
}
3. Use it
<input class="input" type="password" data-show-password>
The toggle appears automatically on every page that has a data-show-password input.
Patterns to follow
- Always use
data-Xattributes, neverids. Multiple instances on a page should work without conflict. - Always use
if (!el) return;guards.init.jsruns on every page, but most pages don’t have most components. - Don’t store state on the DOM element. Use closures or external Maps if you need per-element state.
- Prefer one-shot listeners over delegation. Adminator pages are static; rebinding on dynamic content isn’t a concern. (If you do need delegation, attach to
document.) - Don’t block paint.
init.jsruns afterDOMContentLoaded— heavy work should be deferred viarequestIdleCallbackorsetTimeout(fn, 0).
Recipe — dedicated module (heavy library or stateful)
Goal: integrate a new library (e.g. ECharts) with theme awareness.
1. Create the module
src/assets/scripts/2026/echarts.js:
/**
* ECharts wiring for Adminator.
* Pages declare charts as <div data-echart="key"></div>.
*/
import * as echarts from 'echarts';
function tokens() {
const cs = getComputedStyle(document.documentElement);
return {
primary: cs.getPropertyValue('--primary').trim(),
success: cs.getPropertyValue('--success').trim(),
danger: cs.getPropertyValue('--danger').trim(),
text: cs.getPropertyValue('--t-base').trim(),
muted: cs.getPropertyValue('--t-muted').trim(),
border: cs.getPropertyValue('--border-soft').trim(),
bg: cs.getPropertyValue('--bg-card').trim(),
};
}
const SEEDS = {
'sales-trend': (t) => ({
backgroundColor: 'transparent',
textStyle: { color: t.muted, fontFamily: 'Inter, sans-serif' },
xAxis: { type: 'category', data: ['Mon','Tue','Wed','Thu','Fri'], axisLine: { lineStyle: { color: t.border } } },
yAxis: { type: 'value', splitLine: { lineStyle: { color: t.border } } },
series: [{
type: 'line',
data: [120, 132, 101, 134, 90],
lineStyle: { color: t.primary, width: 2.5 },
areaStyle: { color: t.primary + '22' },
smooth: true,
}],
}),
};
const instances = new Map();
function buildAll() {
const t = tokens();
document.querySelectorAll('[data-echart]').forEach((el) => {
const key = el.getAttribute('data-echart');
const seed = SEEDS[key];
if (!seed) return;
let chart = instances.get(el);
if (!chart) {
chart = echarts.init(el);
instances.set(el, chart);
}
chart.setOption(seed(t), true);
});
}
export function initECharts() {
if (!document.querySelector('[data-echart]')) return;
buildAll();
new MutationObserver((records) => {
if (records.some((r) => r.attributeName === 'data-theme')) buildAll();
}).observe(document.documentElement, { attributes: true });
// Re-render on resize
window.addEventListener('resize', () => instances.forEach((c) => c.resize()));
}
2. Wire it up
In index.js:
import { initECharts } from './echarts.js';
function start() {
mountShell();
initShellBehaviors();
initCharts();
initVectorMaps();
initCalendarPage();
initECharts(); // ← add this
}
3. Use it
<div data-echart="sales-trend" style="width:100%;height:300px;"></div>
The pattern is identical to charts.js / calendar.js / maps.js — copy any of them as a template.
Anti-patterns
Things v3 did that v4 deliberately doesn’t:
| Anti-pattern | Why it’s gone |
|---|---|
class MyComponent { constructor(el) { … } } | Adds 30+ lines for what’s usually a 5-line behavior. Use a function + data-X attribute. |
| Component registry / DI container | Adminator pages are static. There’s no need for runtime composition. |
Custom event bus (adminator:themechange) | MutationObserver on data-theme is built-in, free, and 4 lines. |
| Inline style strings injected via JS | Use SCSS — that’s what stylesheets are for. JS-injected styles defeat the token system. |
Polyfilled Promise / fetch | Browser support is “evergreen modern” — IE11 isn’t on the support matrix. |
jQuery’s $(el).each(...) | document.querySelectorAll(sel).forEach(fn) does the same job natively. |
| Library wrappers that “abstract” Chart.js / FullCalendar | Use them directly. The libraries are already well-designed. |
When to break the conventions
These conventions cover the typical case. If you have a genuinely complex stateful widget (e.g. a kanban board with drag-and-drop, undo, or undo, multi-user cursors), the “data-attribute + init function” pattern is too small. Build a real module with proper state management — but keep the integration with the shell minimal: it should still be initialized from start() and use the tokens() + MutationObserver pattern for theme awareness.
The point of the patterns is to keep simple things simple, not to forbid complexity.