Quick Start
Connect the library to your project in under a minute — no build tools required.
<link> tag in <head>. Nothing else required.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui@1/css/style.min.css">data-theme to the <html> element. All components adapt automatically.
<html lang="en" data-theme="dark">
<!-- dark · light · red · blue · green · cyan · yellow · pink · purple --><!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/njx-ui@1/css/style.min.css">
</head>
<body>
<button class="btn btn-primary">Hello njX</button>
<div class="card-glow">
<h2 class="text-gradient-primary">Works.</h2>
</div>
</body>
</html><!-- Adds: collapseToggle, tabSwitch, dropdownToggle,
openModal, closeModal, showToast, initCarousels -->
<script src="https://cdn.jsdelivr.net/npm/njx-ui@1/js/njx.js"></script>details/summary components work without
JS. See the JavaScript reference for all functions.
Themes
9 built-in themes, each defined as a complete set of CSS custom properties. Switch the entire UI with one attribute — no JavaScript required.
/* Via HTML attribute */
<html data-theme="purple">
/* Via JavaScript */
document.documentElement.setAttribute('data-theme', 'purple');
/* With localStorage persistence */
function setTheme(name) {
document.documentElement.setAttribute('data-theme', name);
localStorage.setItem('theme', name);
}
const saved = localStorage.getItem('theme');
if (saved) setTheme(saved);/* njx-alpine.js preset — load it before Alpine, zero helpers to write */
<script defer src="https://cdn.jsdelivr.net/npm/njx-ui@1/js/njx-alpine.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
/* Toggle dark / light */
<button x-data="njxTheme" @click="toggle()">Toggle theme</button>
/* Set a specific theme */
<div x-data="njxTheme">
<button @click="set('purple')" :class="{ active: t === 'purple' }">Purple</button>
<button @click="set('cyan')" :class="{ active: t === 'cyan' }">Cyan</button>
</div>
/* data-theme on <html> + localStorage persistence — handled by the preset */.btn-primary · .btn-outline
.btn-ghost · .btn-gradient
.btn-glow · .btn-shine
.btn-sm · .btn-lg
.card-dark · .card-bordered
.card-glow · .box
.inp-glass · .float-field
.toggle · .check
.inp-select
.tab-btn · .tab-panel
.tab-nav-pills
.tab-nav-boxed · .tab-nav-card
.tab-nav.is-full · .tab-badge
.collapse + JS toggle
.collapse-plus · .collapse-ghost
.collapse-primary
.accordion · .accordion-flush
.lib-modal-box
.is-success · .is-error
.is-warning · .is-info
.lib-modal-close
.nav-solid · .nav-glass
.nav-primary · .nav-dark
.nav-floating · .nav-underline
.nav-sm · .nav-lg
.dropdown + JS toggle
.dropdown-menu · .dropdown-item
.dropdown-right · .dropdown-top
.dropdown-primary · .dropdown-dark
.carousel-center · .carousel-gap
.carousel-vertical
.carousel-btn · .carousel-dots
.carousel-item-sm/md/lg/xl
.link-slide · .link-arrow
.link-external · .link-muted
.link-btn · .link-card
.link-list
.tag-primary · .tag-success
.tag-accent · .tag-outline
.progress · .progress-bar
.lib-toast · .lib-toast-success
.lib-toast-error · .lib-toast-primary
.text-gradient-primary
.gradient-blue-purple
.hover-brighten · .hover-rotate
njx.js for interactivity. Components marked native use <details>/<summary> and work without any JavaScript.
JavaScript — njx.js
One script file for all interactive components. Include it once — everything initializes automatically.
Connection
<!-- One file, before </body> — everything initializes automatically -->
<script src="https://cdn.jsdelivr.net/npm/njx-ui@1/js/njx.js"></script>
</body>
</html>Which components need JS
| Component | Without JS | With njx.js |
|---|---|---|
| Collapse | details.collapse — работает
нативно | collapseToggle() для div-based
+ аккордеон |
| Dropdown | details.dropdown — работает
нативно | dropdownToggle() для div-based
+ close on outside click |
| Tabs | — | tabSwitch() — переключение панелей |
| Modal | — | openModal() / closeModal()
+ Escape |
| Carousel | Scroll-snap работает, якорные кнопки работают | initCarousels() — синхронизация
dots при свайпе |
| Toast | — | showToast() — динамические уведомления
|
| Buttons, Cards, Forms, Nav, Links, Tags, Table, Progress, Gradients, Animations | ✓ Чистый CSS — JS не нужен | |
Function reference
collapseToggle(header) collapse
Toggles .is-open on the closest .collapse element.
<div class="collapse">
<div class="collapse-header" onclick="collapseToggle(this)">
Title <span class="collapse-icon"></span>
</div>
<div class="collapse-body">Content</div>
</div>accordionToggle(header) accordion
Like collapseToggle but closes all sibling
.collapse items in the same .accordion wrapper first.
<div class="accordion">
<div class="collapse">
<div class="collapse-header" onclick="accordionToggle(this)">Section 1</div>
<div class="collapse-body">...</div>
</div>
<div class="collapse">
<div class="collapse-header" onclick="accordionToggle(this)">Section 2</div>
<div class="collapse-body">...</div>
</div>
</div>dropdownToggle(el) dropdown
Toggles .is-open on a
.dropdown element. All other open dropdowns
are closed first. Outside-click listener is added automatically.
<div class="dropdown">
<button class="btn" onclick="dropdownToggle(this.parentElement)">Menu</button>
<div class="dropdown-menu">
<a class="dropdown-item">Item</a>
</div>
</div>tabSwitch(btn) tabs
Activates the clicked
.tab-btn and shows the corresponding .tab-panel by index inside
.tab-wrap.
<div class="tab-wrap">
<div class="tab-nav tab-nav-pills">
<button class="tab-btn is-active" onclick="tabSwitch(this)">Tab 1</button>
<button class="tab-btn" onclick="tabSwitch(this)">Tab 2</button>
</div>
<div class="tab-content">
<div class="tab-panel is-active">Content 1</div>
<div class="tab-panel">Content 2</div>
</div>
</div>openModal(id) / closeModal(el) modal Opens a modal by its ID / closes by element reference. Escape key auto-closes all open modals. Click on overlay background also closes.
<!-- Trigger -->
<button onclick="openModal('my-modal')">Open</button>
<!-- Modal -->
<div id="my-modal" class="lib-modal-overlay"
onclick="if(event.target===this)closeModal(this)">
<div class="lib-modal-box">
<button class="lib-modal-close"
onclick="closeModal(document.getElementById('my-modal'))">✕</button>
<!-- .is-success .is-error .is-warning .is-info -->
</div>
</div>showToast(msg, type, duration) toast
Shows a toast notification. Types:
success · error · primary · dark. Duration in ms (default
2200). Requires <div
id="lib-toast-container"></div> in the page.
<!-- Required container (place anywhere in body) -->
<div id="lib-toast-container"></div>
// Usage
showToast('Saved!', 'success');
showToast('Something went wrong', 'error', 3000);
showToast('Info message', 'primary');initCarousels() / carouselGo(el, idx) carousel initCarousels() is called automatically on
DOMContentLoaded — it attaches a scroll listener
to every
.carousel-wrap and syncs .carousel-dot.is-active. carouselGo(el, idx)
programmatically scrolls to a slide by 0-based index.
// Scroll to slide index 2 programmatically
const car = document.querySelector('.carousel');
carouselGo(car, 2);
// Re-init after dynamic content (e.g. AJAX)
initCarousels();<details class="collapse"> and <details class="dropdown"> — the browser handles open/close natively, zero JS needed.
Alpine.js — njx.js is optional
Every interactive component works by toggling a single
state class (.is-open, .is-active,
.open). njx.js is one way to toggle it —
Alpine.js is another: keep the same markup and drive the class from
x-data. Ready-made recipes live on the
components page — look for the Alpine.js tab
in the code windows of
Collapse,
Tabs,
Dropdown,
Modal,
Toast and
Carousel.
<!-- Alpine.js from CDN — njx.js becomes optional -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>njx-alpine.js — presets, one word per component
The raw recipes below show how it works. When you
just want it working, ship
js/njx-alpine.js (~3 KB) — it registers
Alpine.data() presets for every interactive
component: njxCollapse,
njxAccordion, njxTabs,
njxDropdown, njxModal,
njxToasts, njxCarousel,
njxTheme. Presets toggle the same classes
njx.js uses, handle outside-click / Escape / body-scroll
for you, and read slide counts from the markup.
<!-- njx-alpine.js: Alpine.data() presets — one word instead of a hand-written object.
Load it BEFORE Alpine (both defer — document order decides). -->
<script defer src="https://cdn.jsdelivr.net/npm/njx-ui@1/js/njx-alpine.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
<!-- before: x-data="{ open: false }" + :class bindings
after: one preset name, classes are synced for you -->
<div class="collapse" x-data="njxCollapse">
<button class="collapse-header" @click="toggle()">Question</button>
<div class="collapse-body">Answer</div>
</div>
<div class="tab-wrap" x-data="njxTabs">
<div class="tab-nav">
<button class="tab-btn" :class="{ 'is-active': isActive(0) }" @click="select(0)">One</button>
<button class="tab-btn" :class="{ 'is-active': isActive(1) }" @click="select(1)">Two</button>
</div>
</div>
<div class="js-carousel-wrap" x-data="njxCarousel(0, 4000)"> <!-- auto count · autoplay 4s -->
<div class="js-carousel">
<div class="js-carousel-track" :style="offset">…slides…</div>
</div>
<button class="js-carousel-btn js-prev" @click="prev()">‹</button>
<button class="js-carousel-btn js-next" @click="next()">›</button>
</div>
<button x-data="njxTheme" @click="toggle()" x-text="t === 'dark' ? '☀️' : '🌙'"></button>
<!-- All presets: njxCollapse · njxAccordion · njxTabs · njxDropdown
njxModal · njxToasts · njxCarousel · njxTheme -->Alpine recipes
x-data="{ open: false }" collapse · accordion
Alpine toggles the same .is-open class
that collapseToggle() would. The
accordion variant keeps one item open via a shared
active index.
<!-- Same markup, same .is-open class — Alpine just toggles it -->
<div class="collapse" x-data="{ open: false }" :class="{ 'is-open': open }">
<div class="collapse-header" @click="open = !open">
Title <span class="collapse-icon"></span>
</div>
<div class="collapse-body">Content</div>
</div>
<!-- Exclusive accordion — one item open at a time -->
<div class="accordion" x-data="{ active: null }">
<div class="collapse" :class="{ 'is-open': active === 1 }">
<div class="collapse-header" @click="active = active === 1 ? null : 1">
Section A <span class="collapse-icon"></span>
</div>
<div class="collapse-body">Content A</div>
</div>
</div>x-data="{ active: null }" accordion
Exclusive open — the shared active index
closes the previous item automatically. Same .is-open class on .collapse.
<!-- Exclusive accordion: one shared index instead of collapseToggle() -->
<div class="accordion" x-data="{ active: null }">
<div class="collapse" :class="{ 'is-open': active === 0 }">
<button class="collapse-header" @click="active = active === 0 ? null : 0">
First question <span class="collapse-icon"></span>
</button>
<div class="collapse-body">Answer one.</div>
</div>
<div class="collapse" :class="{ 'is-open': active === 1 }">
<button class="collapse-header" @click="active = active === 1 ? null : 1">
Second question <span class="collapse-icon"></span>
</button>
<div class="collapse-body">Answer two.</div>
</div>
</div>
<!-- Or the njx-alpine.js preset: x-data="njxAccordion" → toggle(i) / isOpen(i) -->x-data="{ tab: 0 }" tabs
One number of state. Buttons and panels share the
same .is-active binding — no tabSwitch() needed.
<div class="tab-wrap" x-data="{ tab: 0 }">
<div class="tab-nav">
<button class="tab-btn" :class="{ 'is-active': tab === 0 }" @click="tab = 0">Tab 1</button>
<button class="tab-btn" :class="{ 'is-active': tab === 1 }" @click="tab = 1">Tab 2</button>
</div>
<div class="tab-content">
<div class="tab-panel" :class="{ 'is-active': tab === 0 }">Content 1</div>
<div class="tab-panel" :class="{ 'is-active': tab === 1 }">Content 2</div>
</div>
</div>@click.outside dropdown @click.outside replaces the global
document listener that njx.js registers — the menu
closes itself.
<div class="dropdown" x-data="{ open: false }"
:class="{ 'is-open': open }" @click.outside="open = false">
<button class="btn btn-ghost btn-sm" @click="open = !open">
Menu <span class="dropdown-arrow"></span>
</button>
<div class="dropdown-menu">
<a href="#" class="dropdown-item" @click="open = false">Item</a>
</div>
</div>@keydown.escape.window modal
The same .open class on the overlay —
@click.self closes on the backdrop,
Escape closes from anywhere. No openModal/closeModal.
<div x-data="{ open: false }" @keydown.escape.window="open = false">
<button class="btn btn-primary" @click="open = true">Open</button>
<div class="lib-modal-overlay" :class="{ open }" @click.self="open = false">
<div class="lib-modal-box is-primary">
<button class="lib-modal-close" @click="open = false">✕</button>
<div class="lib-modal-title">Title here</div>
<div class="lib-modal-actions">
<button class="btn btn-primary btn-sm" @click="open = false">Confirm</button>
<button class="btn btn-ghost btn-sm" @click="open = false">Cancel</button>
</div>
</div>
</div>
</div>template x-for toast
A toast stack as plain state: fire()
pushes, setTimeout auto-dismisses. Same
.lib-toast classes as showToast().
<div x-data="{
toasts: [], id: 0,
fire(msg, type) {
const t = { id: ++this.id, msg, type };
this.toasts.push(t);
setTimeout(() => this.dismiss(t.id), 2200);
},
dismiss(id) { this.toasts = this.toasts.filter(t => t.id !== id); }
}">
<button class="btn btn-primary btn-sm" @click="fire('Saved!', 'success')">Success</button>
<button class="btn btn-ghost btn-sm" @click="fire('Error occurred', 'error')">Error</button>
<div id="lib-toast-container">
<template x-for="t in toasts" :key="t.id">
<div class="lib-toast" :class="'lib-toast-' + t.type"
@click="dismiss(t.id)" x-text="t.msg"></div>
</template>
</div>
</div>:style="translateX(-i * 100%)" carousel
The track offset is derived from one index — prev /
next / dots all just change i. Works with
.js-carousel-track, .hero-slider-track and .testi-track alike.
<!-- Track position is derived state — one :style binding -->
<div class="js-carousel-wrap" x-data="{ i: 0, n: 3 }">
<div class="js-carousel">
<div class="js-carousel-track"
:style="'transform: translateX(-' + i * 100 + '%)'">
<div>Slide 1</div><div>Slide 2</div><div>Slide 3</div>
</div>
</div>
<button class="js-carousel-btn js-prev" @click="i = (i - 1 + n) % n">‹</button>
<button class="js-carousel-btn js-next" @click="i = (i + 1) % n">›</button>
<div class="js-dots">
<template x-for="d in n" :key="d">
<button class="js-dot" :class="{ 'is-active': i === d - 1 }" @click="i = d - 1"></button>
</template>
</div>
</div>
<!-- Or the njx-alpine.js preset: x-data="njxCarousel(0, 4000)" — autoplay, hover pause, auto slide count -->data-theme + localStorage theme toggle
Dark/light switch in one button — Alpine holds the
state, data-theme does the styling,
localStorage remembers the pick.
<!-- Dark/light toggle with localStorage — one line of state -->
<button class="btn btn-ghost btn-sm"
x-data="{ t: document.documentElement.dataset.theme || 'dark' }"
@click="t = t === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = t;
localStorage.setItem('theme', t)"
x-text="t === 'dark' ? '☀ Light' : '🌙 Dark'"></button>Design Tokens
All values are CSS custom properties defined in _base.css. Use them in your own styles for automatic theme
adaptation.
| Token | Description |
|---|---|
| --color-primary | Main brand color (theme-reactive) |
| --color-accent | Secondary accent |
| --color-success | Green / success state |
| --color-error | Red / error state |
| --color-warning | Orange / warning state |
| --color-light | Primary text |
| --color-muted | Secondary / muted text |
| --сolor-dark | Page background |
| --color-dark-secondary | Card / surface background |
| Token | Value |
|---|---|
| --space-1 | 4px |
| --space-2 | 8px |
| --space-3 | 12px |
| --space-4 | 16px |
| --space-6 | 24px |
| --space-8 | 32px |
| --space-12 | 48px |
| --space-16 | 64px |
| Token | Value |
|---|---|
| --fs-xs | 12px |
| --fs-sm | 14px |
| --fs-base | 16px |
| --fs-lg | 18px |
| --fs-xl | 20px |
| --fs-2xl | 24px |
| --fs-3xl | 30px |
| --fs-4xl | 36px |
| --fs-5xl | 48px |
| --radius-none | 0 |
| --radius-sm | 6px |
| --radius-md | 12px |
| --radius-lg | 20px |
| --radius-full | 9999px |
| --ease-fast | 0.1s ease |
| --ease | 0.2s ease |
| --ease-slow | 0.4s ease |
Using Design Tokens
How to write CSS that works with all 9 themes automatically.
Colors — --color-*
Never hardcode a hex value. Use a token — and when the theme changes, the color updates automatically everywhere.
.btn {
color: #14a0ff;
border: 1px solid #14a0ff;
background: rgba(20,160,255,0.1);
}.btn {
color: var(--color-primary);
border: 1px solid var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
}
Spacing — --space-*
4px base grid. Index = multiplier × 4px.
4px
8px
12px
16px
24px
32px
48px
64px
.header { padding: var(--space-4) var(--space-6); }
.card { padding: var(--space-6); margin-bottom: var(--space-4); }
.layout { gap: var(--space-4); }
Typography — --fs-*
12px
14px
16px
18px
20px
24px
30px
36px
48px
.label { font-size: var(--fs-xs); text-transform: uppercase; }
.body { font-size: var(--fs-base); line-height: 1.6; }
.heading { font-size: var(--fs-3xl); font-weight: 800; }
Font Families — --font-*
--font-sans, --font-heading or --font-mono in your own CSS — all components
update automatically.
Radius & Shadows — --radius-* /
--shadow-*
0
6px
12px
20px
9999px
.btn { border-radius: var(--radius-sm); box-shadow: var(--shadow-sm); }
.card { border-radius: var(--radius-md); box-shadow: var(--shadow-md); }
.modal { border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); }
.glow { box-shadow: var(--shadow-primary); }
Transitions — --ease-*
var(--ease-fast) /* 0.1s ease — for hover states */
var(--ease) /* 0.2s ease — standard default */
var(--ease-slow) /* 0.4s ease — smooth reveals */
.btn { transition: all var(--ease); }
.card { transition: transform var(--ease), box-shadow var(--ease); }Full component — tokens only
.game-card {
padding: var(--space-6);
border-radius: var(--radius-md);
background: var(--color-dark-secondary);
box-shadow: var(--shadow-md);
}
.game-card__label {
font-size: var(--fs-xs);
color: var(--color-primary);
}
.game-card__title {
font-size: var(--fs-2xl);
font-weight: 800;
color: var(--color-light);
}Utility Classes
Tailwind-compatible utility classes for fast layout.
All in _utils.css.
<div class="flex items-center gap-4 p-6">
<button class="btn btn-primary">Action</button>
<p class="text-sm text-muted">Helper text</p>
</div>
<div class="flex-center gap-6 mt-8 mb-4">
<span class="tag tag-primary uppercase text-xs">New</span>
<h2 class="text-2xl font-bold text-primary">Title</h2>
</div>Animations
Built-in CSS animations from _animations.css. Just add a class — no JavaScript.
.fade-out
.scale-up
.slide-down
.bounce-in
.pulse-slow
.glow-hover
.spin-slow
.shake-error
.animate-neon-blue
.animate-breathe
.anim-fast · .anim-slow
<div class="fade-in">Fades in on load</div>
<button class="btn btn-primary animate-pulse">Pulsing CTA</button>
<div class="animate-float">Floating element</div>
<span class="animate-neon text-accent">Neon glow text</span>
<div class="scale-in anim-delay-1">Delayed scale-in</div>File Structure
All CSS is modular — import everything via style.css or pick individual files.
njx-ui/ ├── style.css ← entry point (@import all modules) ├── css/ │ ├── _base.css ← design tokens (CSS variables) │ ├── _reset.css ← CSS reset │ ├── _global.css ← 9 theme definitions │ ├── _grid.css ← .row/.col-* and .grid-col-* │ ├── _typography.css ← headings, text helpers │ ├── _utils.css ← Tailwind-style utility classes │ ├── _buttons.css ← .btn, .btn-primary, gradients, glow │ ├── _cards.css ← .card, .card-glass, .card-glow, .box │ ├── _form.css ← .inp, .toggle, .check, .float-field │ ├── _tags.css ← .tag, .badge, outline variants │ ├── _nav.css ← .nav, solid/glass/dark/floating/underline │ ├── _tab.css ← .tab-wrap, pills/boxed/card variants │ ├── _collapse.css ← .collapse, accordion, accordion-flush │ ├── _dropdown.css ← .dropdown, positions, JS+native API │ ├── _slider.css ← .carousel, scroll-snap, dots, gap │ ├── _popups.css ← .lib-modal-*, success/error/warning/info │ ├── _links.css ← .link, slide/arrow/btn/card/list variants │ ├── _notifications.css ← .lib-toast, alerts │ ├── _table.css ← .table and variants │ ├── _breadcrumb.css ← .breadcrumb │ ├── _sections.css ← .section, .panel, layout sections │ ├── _animations.css ← fade, pulse, float, neon, etc. │ ├── _gradients.css ← bg-gradient-*, text-gradient-* │ └── _hovers.css ← hover-lift, hover-glow, etc. └── js/ └── njx.js ← collapse, tabs, modal, carousel…
_base.css + _themes.css + _buttons.css
+ _cards.css and save several KB.
Examples
Common patterns — copy and adapt.
<div class="card card-glass p-6">
<h3 class="text-gradient-primary">Card Title</h3>
<p class="text-muted mt-2">Description text...</p>
<div class="flex gap-4 mt-6">
<button class="btn btn-primary btn-sm">Action</button>
<button class="btn btn-outline btn-sm">Cancel</button>
</div>
</div><div class="field">
<label class="field-label">Email</label>
<input class="inp inp-error" type="email" placeholder="[email protected]">
<span class="field-error-msg">Invalid email address</span>
</div>
<div class="field">
<label class="field-label">Password</label>
<input class="inp inp-success" type="password">
<span class="field-success-msg">Password accepted</span>
</div><div class="lib-modal-overlay" id="modal">
<div class="lib-modal-box is-success">
<span class="lib-modal-icon">✓</span>
<div class="lib-modal-title">Success!</div>
<div class="lib-modal-text">Operation completed</div>
<div class="lib-modal-actions">
<button class="btn btn-success"
onclick="document.getElementById('modal').remove()">OK</button>
</div>
</div>
</div><div x-data="{ open: true }" @keydown.escape.window="open = false">
<div class="lib-modal-overlay" :class="{ open }" @click.self="open = false">
<div class="lib-modal-box is-success">
<span class="lib-modal-icon">✓</span>
<div class="lib-modal-title">Success!</div>
<div class="lib-modal-text">Operation completed</div>
<div class="lib-modal-actions">
<button class="btn btn-success" @click="open = false">OK</button>
</div>
</div>
</div>
</div>