Sortable tables, date pickers, modals, and 43 more components. In a script tag, in the template you already have.
No Node. No build step. No frontend toolchain. Drop them into a Django, Rails or Laravel template, or plain HTML.
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/dist/rnx.global.js"></script>
<DataTable sortable filterable></DataTable>That is the whole setup. No npm install, no bundler, no package.json.
Link rnx.css as well and that markup renders like this, with no styling of
your own:
Every screen above is rnxJS components in a plain HTML file. There is a live version at playground.baryo.dev/rnxjs.
You write Django, Rails, Laravel or Express. You need an admin screen with a sortable table and a date picker by Friday. You do not want to stand up a separate React repo, learn a build pipeline, and maintain two deployments to get there.
rnxJS gives you the components without the toolchain. It renders into the templates you already serve.
Not for you if: you are building a large single-page application. Use a real framework. rnxJS is for server-rendered apps that need interactive pieces.
Data and tables
DataTable VirtualList List Pagination StatCard Search Autocomplete
Forms
Input Textarea Select Checkbox Radio Switch Slider DatePicker FileUpload FormGroup SegmentedButton Chips
Navigation
NavigationBar NavigationDrawer Sidebar TopAppBar Breadcrumb Tabs Stepper Dropdown
Feedback and state
Alert Toast Modal Tooltip Spinner ProgressBar Skeleton EmptyState ErrorState ErrorBoundary Badge
Layout
Container Row Column Card Accordion
Actions
Button FAB Icon
Full reference with props and examples: Component Library
Components come unstyled by default and take their look from a stylesheet.
Link rnx.css and you get a considered default without making any decisions:
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/css/rnx.css" rel="stylesheet">Everything below is optional. Take it in the order that suits you: most people never get past the first step.
rnx.css is built entirely from CSS custom properties. Set them in your own
stylesheet, after the link above, and every one of the 46 components follows.
No JavaScript, no build step, no forking:
:root {
--rnx-primary: #7c3aed;
--rnx-border-radius: 12px;
--rnx-font-family: "Inter", system-ui, sans-serif;
}That is the whole of theming for most projects. Buttons, tables, inputs, modals, the focus rings and the page furniture all recolour together.
The full list lives in css/themes/base.css. The ones
worth knowing:
| Token | Controls |
|---|---|
--rnx-primary |
the accent, everywhere |
--rnx-success --rnx-danger --rnx-warning |
status colours, deliberately separate from the accent |
--rnx-background --rnx-surface |
the page, and the panels on it |
--rnx-text-primary --rnx-text-secondary |
ink |
--rnx-border-color --rnx-border-radius |
lines and corners |
--rnx-font-family --rnx-font-family-display --rnx-font-family-monospace |
body, headings, code |
Dark values are built in. See Dark mode below.
Every component accepts className, and those classes are applied last so they
win:
<Button variant="primary" label="Save" className="my-save-button" />.my-save-button { letter-spacing: 0.04em; text-transform: uppercase; }If you are not using Bootstrap at all, a theme is a map of class strings per component. This is how the built-in Tailwind theme works:
import { registerTheme, setTheme } from '@arnelirobles/rnxjs';
registerTheme({
name: 'my-theme',
components: {
button: {
base: 'my-btn',
variants: { primary: 'my-btn-primary' },
sizes: { sm: 'my-btn-sm', md: '', lg: 'my-btn-lg' }
}
}
});
setTheme('my-theme');Components are theme-agnostic, so the same markup renders correctly under any registered theme:
<Button variant="primary" label="Save changes"></Button>
<!-- bootstrap: class="btn btn-primary" -->
<!-- tailwind: class="inline-flex items-center ... bg-[color:var(--rnx-primary,#4f46e5)] ..." -->Switch with setTheme('bootstrap') or setTheme('tailwind').
Tailwind only generates classes it finds in your source, and the Tailwind
theme's classes live inside node_modules. Without scanning them you get an
unstyled app (for example sr-only on the Spinner label is never generated, so
"Loading..." shows as visible text). Classes are in the component JS as well as
themes/tailwind/index.js, so the glob must cover the whole package.
Tailwind 3 does not merge a preset's content with yours, your own content
replaces it. So the preset exports its globs and you spread them into your
array. The preset itself adds the toast entrance animation:
// tailwind.config.js
import rnx, { content } from '@arnelirobles/rnxjs/tailwind';
export default {
presets: [rnx],
content: ['./src/**/*.{html,js}', ...content]
};CommonJS works too:
const rnx = require('@arnelirobles/rnxjs/tailwind');
module.exports = {
presets: [rnx],
content: ['./src/**/*.{html,js}', ...rnx.content]
};Or skip the preset and add the glob by hand (no toast animation):
content: [
'./src/**/*.{html,js}',
'./node_modules/@arnelirobles/rnxjs/**/*.js' // required
]Without the preset the toast still works, it just appears without the slide-in.
Every colour in the Tailwind theme reads an --rnx-* CSS variable and falls
back to the Tailwind colour it used before, for example
bg-[color:var(--rnx-primary,#4f46e5)]. Set the variables in your own CSS. No
Tailwind config change, no fork:
:root {
--rnx-primary: #7c3aed;
--rnx-primary-hover: #6d28d9;
--rnx-primary-active: #5b21b6;
}Linking css/rnx.css feeds its colours into the Tailwind theme, so the two
themes look alike without extra work. Set a variable after it to override one.
Token values must be full colours (#7c3aed, rgb(124 58 237)), not
space-separated triplets like 79 70 229.
With nothing set, every colour in the theme resolves to the Tailwind colour it
had before, exactly (a rounding of 1 per channel at most in the color-mix()
tints). tests/tailwindDefaults.test.js pins this against a frozen table.
--rnx-background is the page and --rnx-surface is the card. Cards, inputs,
modals, dropdowns and table rows use --rnx-surface. Hover and footer fills use
--rnx-surface-hover.
Tokens it reads:
--rnx-primary,-hover,-active--rnx-secondary--rnx-success,--rnx-danger,--rnx-warning,--rnx-info, each with-hover--rnx-surface,--rnx-surface-hover,--rnx-surface-variant--rnx-text-primary,--rnx-text-secondary,--rnx-text-disabled--rnx-text-on-primary,--rnx-text-on-warning(the label on the warning button)--rnx-border-color,--rnx-border-color-light
Tints and shades that have no token of their own (the light alert backgrounds,
focus rings) are color-mix() of one of these with --rnx-surface or
--rnx-text-primary, so they follow when you change the token. This needs a
browser with color-mix() (Chrome 111, Safari 16.2, Firefox 113 or newer).
The select chevron is slate (#64748b) and does not follow tokens. The
checkbox tick and radio dot are white in light mode and #0b1216 in dark mode,
set in the theme, not by tokens. The ring offset follows --rnx-surface.
import { setMode } from '@arnelirobles/rnxjs';
setMode('dark'); // 'light' (default), 'dark' or 'system'setMode sets data-mode and data-bs-theme on <html> and saves the
choice in localStorage (rnx-mode). css/rnx.css has a dark token block on
[data-mode="dark"] (and [data-theme="dark"]), so without JavaScript you can
write <html data-mode="dark"> yourself. 'system' follows the OS setting
live. getMode() returns the choice and getResolvedMode() returns 'light'
or 'dark'. No stylesheet has a prefers-color-scheme rule; the mode comes
from the attribute only.
- Bootstrap's own dark styling (
bg-light, navbars,btn-light) followsdata-bs-theme, which needs Bootstrap 5.3 or newer. - A saved choice overrides a
data-modeattribute written in the HTML. - To avoid a light flash, put this in
<head>before the stylesheets:
<script>
try {
var m = localStorage.getItem('rnx-mode');
if (m === 'system') m = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
if (m === 'light' || m === 'dark') {
document.documentElement.setAttribute('data-mode', m);
document.documentElement.setAttribute('data-bs-theme', m);
}
} catch (e) {}
</script>- Loading
css/themes/base.csstogether withcss/rnx.cssis not supported for dark mode. Use one of them. - A brand override in
:root(say--rnx-primary: #7c3aed) also applies in dark mode, because both rules have the same weight and yours comes later. If it should change in dark, add a matching[data-mode="dark"]override.
Changing a component's internal markup. Autocomplete and VirtualList accept
a renderItem function; nothing else does. Slots are
tracked separately.
Welcome to rnxJS! In this 5-minute tutorial, we'll build a reactive Employee Directory with a search filter. No Webpack, no Bundlers, just HTML and JS.
Create an index.html file and include Bootstrap + rnxJS.
<!DOCTYPE html>
<html>
<head>
<title>rnxJS App</title>
<!-- 1. Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 2. Bootstrap Icons (needed for the icon prop) -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<!-- 3. rnxJS M3 Theme (Optional, for Material Styling) -->
<link href="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/css/bootstrap-m3-theme.css" rel="stylesheet">
</head>
<body class="bg-light">
<!-- App Container -->
<Container class="py-5" id="app">
<!-- We will put our content here -->
</Container>
<!-- 4. rnxJS Library -->
<script src="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/dist/rnx.global.js"></script>
<script src="app.js"></script>
</body>
</html>Create app.js. We'll initialize our Reactive State.
// app.js
const { createReactiveState, autoRegisterComponents, loadComponents } = rnx;
// 1. Define your data model
const state = createReactiveState({
searchQuery: '',
employees: [
{ id: 1, name: 'Alice Johnson', role: 'Engineer', dept: 'Tech' },
{ id: 2, name: 'Bob Smith', role: 'Designer', dept: 'Creative' },
{ id: 3, name: 'Charlie Kim', role: 'Manager', dept: 'Sales' },
],
// Computed property (derived state works by manually updating or logical getters)
// For simplicity in rnxJS v0, we handle filtering in the view or listeners
});
// 2. Register Bootstrap Components
autoRegisterComponents();
// 3. Hydrate the DOM
loadComponents(document.body, state);Update the <Container> in index.html. We use data-bind to sync inputs and text.
<Container class="py-5" id="app">
<Card class="mb-4">
<h2 class="mb-3">Employee Directory</h2>
<!-- Search Input: Two-way binding to 'searchQuery' -->
<FormGroup>
<Input
placeholder="Search employees..."
data-bind="searchQuery"
/>
<small class="text-muted">
Searching for: <span data-bind="searchQuery" class="fw-bold"></span>
</small>
</FormGroup>
</Card>
<Row id="employee-list">
<!-- We will render the list here dynamically -->
</Row>
<!-- Floating Action Button -->
<FAB icon="add" variant="primary" onclick="alert('Add Employee Clicked!')"></FAB>
</Container>rnxJS works great with vanilla JS logic. Let's add a listener to filter the list.
// Add this to app.js
function renderList() {
const listContainer = document.getElementById('employee-list');
const query = state.searchQuery.toLowerCase();
// Filter logic
const filtered = state.employees.filter(emp =>
emp.name.toLowerCase().includes(query) ||
emp.role.toLowerCase().includes(query)
);
// Vanilla JS rendering (fast and simple)
listContainer.innerHTML = filtered.map(emp => `
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">${emp.name}</h5>
<h6 class="card-subtitle mb-2 text-muted">${emp.dept}</h6>
<p class="card-text">${emp.role}</p>
</div>
</div>
</div>
`).join('');
}
// Subscribe to search changes to re-render
state.subscribe('searchQuery', renderList);
// Initial render
renderList();🎉 That's it! You have a reactive app with search and Bootstrap styling.
The heart of rnxJS is the createReactiveState function. It wraps your object in a Proxy to detect changes.
const state = rnx.createReactiveState({
user: { name: 'Arnel', points: 100 },
items: ['Apple', 'Banana']
});Key Features:
- Deeply Nested: Works on
state.user.name. - Arrays:
push,pop,splicetrigger updates automatically. state.subscribe(path, callback): Listen for changes.- Path examples:
'user.name','items','items.0'.
- Path examples:
state.$unsubscribeAll(): Cleanup all listeners (useful for Single Page Apps).
Connect your DOM to State without event listeners.
| Element | Binding Type | Behavior |
|---|---|---|
<input>, <textarea> |
Two-Way | Updates state on typing; updates value on state change. |
<select> |
Two-Way | Updates selection state. |
<checkbox> |
Two-Way | Binds to boolean state. |
<div>, <span>, <p> |
One-Way | Updates textContent when state changes. |
Validation (data-rule):
Add rules to inputs to populate state.errors.
<input data-bind="email" data-rule="required|email" />
<span class="text-danger" data-bind="errors.email"></span>Rules: required, email, numeric, min:5, max:10, pattern:^A.*.
rnxJS provides 46 production-ready components.
Standard: <Button>, <Card>, <Modal>, <Alert>, <Badge>, <Spinner>, <Toast>.
Forms: <Input>, <Checkbox>, <Radio>, <Select>, <Textarea>, <Switch>, <Slider>, <FileUpload>.
Layout: <Container>, <Row>, <Column>, <Sidebar>.
Material (M3): <FAB>, <Chips>, <NavigationDrawer>, <TopAppBar>, <List>, <Icon>.
Advanced: <DataTable>, <Dropdown>, <ProgressBar>, <Stepper>, <Tooltip>, <Tabs>, <Accordion>, <Pagination>.
Usage:
- Auto Register:
rnx.autoRegisterComponents()registers all of them. - Manual Register:
rnx.registerComponent('MyBtn', Button). - Props: Attributes are passed as props.
data-bindworks on components too!<Input label="Name" data-bind="user.name" /> <!-- Renders a labeled input group bound to user.name -->
When creating custom components, use hooks to manage resources.
const component = createComponent(templateFn, props);
component.useEffect((self) => {
console.log('Mounted!');
const interval = setInterval(() => console.log('Tick'), 1000);
// Return cleanup function (called on unmount)
return () => clearInterval(interval);
});
component.onUnmount(() => {
console.log('Destroyed');
});For a clean codebase, we recommend this folder structure:
/
├── index.html # Entry point
├── css/
│ └── styles.css # Custom styles / overlays
├── js/
│ ├── app.js # Main logic (State init, Load)
│ ├── components/ # Custom components
│ │ └── UserCard.js
│ └── utils/ # Helpers
└── assets/
Create reusable functional components:
// js/components/UserCard.js
import { createComponent } from '@arnelirobles/rnxjs';
export function UserCard({ name, role }) {
// Template
const template = (state) => `
<div class="card shadow-sm">
<div class="card-body">
<h3>${name}</h3>
<p class="text-muted">${role}</p>
</div>
</div>
`;
return createComponent(template);
}
// Register it
import { registerComponent } from '@arnelirobles/rnxjs';
registerComponent('UserCard', UserCard);Use it in HTML: <UserCard name="John" role="Dev"></UserCard>
Use unpkg or jsdelivr.
<!-- Library -->
<script src="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/dist/rnx.global.js"></script>
<!-- M3 Theme CSS -->
<link href="https://cdn.jsdelivr.net/npm/@arnelirobles/rnxjs/css/bootstrap-m3-theme.css" rel="stylesheet">npm install @arnelirobles/rnxjsimport { createReactiveState, loadComponents } from '@arnelirobles/rnxjs';
import '@arnelirobles/rnxjs/css/bootstrap-m3-theme.css'; // Optional M3 theme| Feature | rnxJS | jQuery | Vue 3 | React 18 |
|---|---|---|---|---|
| Bundle Size (gzipped) | ~42KB (includes 46 components) | ~30KB | ~16KB | ~42KB |
| Zero Build Required | ✅ Yes | ✅ Yes | ❌ Required | |
| Built-in Components | 46 | 0 | 0 | 0 |
| Two-Way Binding | ✅ Built-in | ❌ Manual | ✅ v-model | ❌ Manual |
| Form Validation | ✅ Built-in | ❌ Plugin | ❌ Library | ❌ Library |
| Learning Curve | 1 hour | 1 hour | 1 day | 1 week |
| Backend Integration | ✅ Django/Rails/Laravel/Express | ✅ Any |
Perfect for:
- Backend Devs: Django/Rails/Laravel developers who want interactivity without a separate SPA repo.
- Internal Tools: Rapidly build admin panels using standard Bootstrap.
- Prototypes: "Zero to Hero" in 5 minutes.
- jQuery Migrations: Modern reactivity with similar simplicity.
See full benchmarks for detailed performance comparisons.
rnxJS uses Bootstrap Icons by default. Ensure you include the Bootstrap Icons stylesheet in your project:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">When using the icon prop in components like Button, FAB, Icon, etc., simply provide the icon name (e.g., moon-stars, check-circle). The library applies the bi bi-[name] classes.
<Button icon="moon-stars" label="Theme" />
<Icon name="check-circle" color="text-success" />To use another icon set, give the theme an icon mapping. Every component and the toast plugin read it:
import { tailwindTheme, setTheme } from '@arnelirobles/rnxjs';
tailwindTheme.utilities.icon.className = (name) => `ph ph-${name}`; // Phosphor
setTheme('tailwind');A custom theme sets utilities.icon.className the same way. Without one, it falls back to Bootstrap Icons. Icon names are passed through as given, so use names your set has.
| Guide | Description |
|---|---|
| 📰 v1.0.0 Release Post | Complete overview of v1.0.0 features, benchmarks, and comparisons |
| Quick Start | Get started in 5 minutes |
| Component Library | Complete reference for all 46 components with examples |
| API Reference | Complete API documentation with stability guarantees |
| Migration Guide | Migrate from jQuery to rnxJS |
| Benchmarks | Performance comparisons with jQuery, Vue, React |
- Ensure you have called
rnx.autoRegisterComponents()or manually registered it viarnx.registerComponent('FAB', FAB). - Check that the Bootstrap Icons stylesheet is included if icons are missing. rnxJS uses Bootstrap Icons (
bi bi-*), not Material Symbols. - If using
data-if, ensure the condition evaluates to true.
- As of v0.3.4,
data-bindis synchronous. EnsureloadComponents(document, state)is called after the DOM is ready (e.g., at the end of<body>or insideDOMContentLoaded). - Check your browser console for warnings like
[rnxJS] Invalid data-bind path. - Ensure your state object was created with
createReactiveState.
- Use
setBootstrap(window.bootstrap)if you are using a bundler and Bootstrap isn't attached to the global window object.
- We welcome contributions! Please verify potential changes with existing tests:
npm test.
- Tests: 696 tests across 39 files, running in under 4 seconds. 690 pass; 6 are known failures with open issues against them (#23, #24, #25).
- CI: not yet running the suite on pull requests. Tracked in #2.
- Browser support: all modern browsers (Chrome, Firefox, Safari, Edge).
- Dependencies: the CDN build needs only Bootstrap CSS.
- Maintenance: actively maintained, not actively expanded. The component count is deliberately frozen at 46. Bug reports and pull requests are welcome and get read.
Working examples: rnxJS_samples
Issues labelled good first issue carry the file, the line, and the actual failing assertion, so they are pickable without rediscovering anything.
Before opening a pull request, run npm test. Note the 6 known failures above; if your change adds an eighth, that one is yours.
Full changelog: CHANGELOG.md
MPL-2.0 © Arnel Isiderio Robles
