The batteries I wish vanilla JavaScript came with.
A small utility belt for DOM work, routing, and browser-side glue code.
·
Report Bugs
·
Request Features
IMPORTANT: Things are changing a lot right now, so please be patient, this is a work in progress
- Table of Contents
- About The Project
- Getting Started
- API Stability
- Documentation
- Contributing
- License
- Contact
- Roadmap
- Acknowledgements
Paradox is a vanilla JavaScript utility library for people who want a little more help from the browser without committing to a full framework.
The goal is simple: make repetitive client-side work easier while staying close to platform APIs. Think DOM creation helpers, a lightweight router, pub/sub utilities, and a few carefully chosen browser-side batteries.
Paradox is not trying to replace React, Vue, Angular, or the DOM itself. If a feature pushes the project toward "framework" territory, it should earn its place very carefully.
Contributions are welcome. Please start a discussion if you want to propose a change so we can keep the scope intentional.
Link to the npm package
Paradox is a small vanilla JavaScript utility library that currently provides:
- DOM creation helpers
- DOM enhancement helpers for existing markup
- A simple router
- A lightweight pub/sub implementation
- An experimental reactive runtime that may change significantly
- Node.js >= v16.16.0 (For development mode)
- Clone the repo
git clone git@github.com:rincorpes/paradoxjs.git
- Install NPM packages
cd paradoxjs npm install
Paradox is meant to be adopted incrementally. Use the parts that help, ignore the rest.
You can use Paradox in any modern frontend toolchain. A small Vite setup is the easiest place to start.
Then you can install it like this:
npm i @rincorpes/paradoxjsThen you can import it like this:
import Paradox from "@rincorpes/paradoxjs";Now you'll have access to Paradox in your project.
Here's an example of a project structure that uses Paradox with Vite:
. # root
├── src
│ ├── main.js
│ └── style.scss
├── dist
├── index.html
├── node_modules
├── package.json
├── package-lock.json
└── vite.config.js- The
srcfolder contains the app entry point and styles where you can import Paradox. - The
distfolder contains the production build generated by Vite. - The
index.htmlfile is the HTML entry used by Vite during development and build. - The
node_modulesfolder contains the npm packages for the project. - The
package.jsonfile contains the project info and the npm scripts. - The
package-lock.jsonfile contains the npm packages info. - The
vite.config.jsfile contains the Vite config.
import { defineConfig } from "vite";
export default defineConfig({
server: {
host: "localhost",
port: 3040,
},
preview: {
host: "localhost",
port: 3040,
},
});Paradox is currently packaged for npm-based tooling with a single root entry point and an exports map. For package consumers, prefer:
import Paradox from "@rincorpes/paradoxjs";This works well in modern tooling such as bundlers, test runners, and Node-aware build pipelines.
Paradox does not currently ship a browser-native ESM bundle for direct <script type="module"> usage without a build step. If that becomes an important use case again, it should be added as a separate package target rather than relying on internal output files.
Avoid relying on deep package paths, since they are considered internal and are not part of the supported public package surface.
Paradox currently uses three API stability tiers.
These are the supported public APIs for this iteration:
Paradox.buildElementParadox.delegateParadox.RouterParadox.pubsub- The default
Paradoxexport from@rincorpes/paradoxjs
These utilities reflect the main direction of the library as a vanilla JavaScript utility belt.
These APIs exist, but may change significantly or be removed while the project is refined:
Paradox.buildApp
buildApp is currently an experiment in reactive rendering. It is not yet part of the stable core identity of Paradox.
Anything outside the top-level package entry should currently be treated as internal and unsupported for external use. This includes:
- Deep imports such as
@rincorpes/paradoxjs/build/core/... - Source-level imports such as
src/core/... - Helper modules under
core/buildApp/*andcore/buildElement/* - Internal types that are not exported from the package root
Those paths expose implementation details rather than supported public API. The only documented consumer import style for this iteration is the package root:
import Paradox from "@rincorpes/paradoxjs";Paradox does not yet have a finalized stable type export strategy. For now:
- Runtime imports from the package root are supported
- Deep-imported types should be treated as internal
- If you need app-specific helper types today, prefer defining them in your own project instead of importing Paradox internals
- Type exports will be revisited as part of the packaging and API cleanup work
Paradox currently includes the following public utilities:
Paradox provides a small DOM helper with the buildElement function. It is meant to reduce repetitive DOM setup work while staying close to browser primitives.
Properties:
| Property | Type | Description |
|---|---|---|
| id | string | The element id |
| className | string | string[] | Class name(s) for the element |
| classList | string | string[] | Additional class name(s) for the element |
| attributes | object | Standard HTML attributes |
| data | object | data-* attributes using camelCase or snake_case keys |
| aria | object | aria-* attributes using camelCase or snake_case keys |
| events | object | Event listeners, including arrays of handlers |
| style | object | Inline styles using camelCase keys or CSS custom properties |
| text | string | number | Text appended before child nodes |
| children | array | Strings, numbers, existing nodes, descriptors, or nested arrays |
import Paradox from "@rincorpes/paradoxjs";
const button = Paradox.buildElement("button", {
className: ["btn", "btn-primary"],
attributes: {
type: "button",
},
data: {
trackingId: "cta-1",
},
aria: {
label: "Open details",
},
events: {
click: [
() => console.log("primary click"),
() => console.log("analytics event"),
],
},
text: "Open details",
});
const card = Paradox.buildElement("section", {
id: "hero-card",
classList: "card shadow-sm",
style: {
padding: "1rem",
"--accent-color": "#ef4444",
},
children: [
Paradox.buildElement("h2", { text: "Paradox" }),
"The batteries I wish vanilla JavaScript came with.",
button,
],
});
document.body.appendChild(card);NOTES:
- The
buildElementfunction returns an actual DOM element. - Children can be plain text, DOM nodes, nested arrays, or legacy
{ tag, options }descriptors. - Event entries can be a single handler or an array of handlers.
dataandariakeys are translated intodata-*andaria-*attributes automatically.
Paradox.delegate helps you attach behavior to existing markup without re-rendering it. This is useful when your HTML already exists and you just want a clean way to wire interactions.
import Paradox from "@rincorpes/paradoxjs";
Paradox.delegate(document, {
click: {
'[data-role="refresh-dashboard"]': async (_event, button) => {
const originalLabel = button.textContent?.trim() || "Refresh preview";
button.setAttribute("disabled", "");
button.textContent = "Refreshing...";
try {
await refreshDashboard();
} finally {
button.removeAttribute("disabled");
button.textContent = originalLabel;
}
},
},
});NOTES:
delegateworks well with static HTML anddata-*selectors.- Handlers receive both the native event and the matched element.
- You can register multiple handlers for the same selector by passing an array.
- The function returns a cleanup callback that removes the delegated listeners.
Paradox.Router is a small History API router for simple SPAs. It handles route matching, same-origin link interception, popstate, and route props injection without trying to become an application framework.
import Paradox from "@rincorpes/paradoxjs";
function Home({ root }) {
root.replaceChildren(
Paradox.buildElement("div", {
children: [
Paradox.buildElement("h1", {
text: "Welcome to Paradox!",
}),
Paradox.buildElement("a", {
text: "About",
attributes: {
href: "/about?name=Paradox",
},
}),
],
})
);
}
function About({ root, query }) {
root.replaceChildren(
Paradox.buildElement("div", {
children: [
Paradox.buildElement("h1", {
text: `About ${query?.get("name") || ""}`.trim(),
}),
Paradox.buildElement("a", {
text: "Home",
attributes: {
href: "/",
},
}),
],
})
);
}
const root = document.getElementById("root");
const baseUrl = document.querySelector("base")?.href || window.location.origin;
const routes = [
{
path: "/",
component: Home,
props: { root },
},
{
path: "/about",
component: About,
props: { root },
},
];
const router = new Paradox.Router({ routes, baseUrl });
router.init().catch((error) => {
console.error(error);
});You can also navigate in code:
router.navigate("/about?name=Paradox");Route handlers receive the props you pass plus a few router-specific values:
queryString: The current query string, like?name=Paradoxquery: AURLSearchParamsinstance for the current URLparams: AMapof dynamic route params, likeidfrom/users/:idbaseUrl: The router base URLpath: The matched pathroute: The matched route definitionrouter: The active router instance
If a route component or layout returns a function, Paradox will call it before the next route renders. That gives you a tiny cleanup hook for removing listeners, unsubscribing, or tearing down page-specific work.
Scope notes:
- This router is meant for simple browser navigation, not nested routing, data loaders, transitions, or SSR concerns.
- After
init(), same-origin links inside the app are intercepted automatically and routed through the History API. - If the current URL uses a
#/pathhash, Paradox will still resolve it on load for backward compatibility, but regular path-based URLs are the preferred mode for this iteration.
Paradox provides a simple PubSub implementation to handle communication between independent parts of a page or app.
In case you're not familiar, PubSub is a popular messaging pattern in software architecture. It stands for Publish-Subscribe pattern and is used for communication between different parts of an application or between different applications.
In the PubSub pattern, publishers send messages without knowing who the subscribers are. Subscribers, on the other hand, express interest in one or more events and only receive messages that are of interest, without knowing who sent them.
This pattern is widely used in event-driven programming and can help decouple different parts of an application, leading to code that is easier to maintain and extend.
import Paradox from "@rincorpes/paradoxjs";
Paradox.pubsub.subscribe("myEvent", (data) => {
console.log(data);
});
Paradox.pubsub.publish("myEvent", "Hello World!");NOTES:
- You can use pubsub to communicate between separate modules, widgets, or route handlers without wiring them directly together.
- Subscriptions are not persistent, so if you subscribe to an event and then navigate to another page, the subscription will be lost.
- You can use the
Paradox.pubsub.unsubscribemethod to unsubscribe from an event. - You can not publish an event before subscribing to it xd.
You can find some examples in the examples folder.
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.
Distributed under the MIT License. See LICENSE for more information.
Santiago Rincon - @alexsc6955
See the ROADMAP.md for a list of proposed features (and known issues).
If you have any ideas, please start a discussion and we can figure it out together.
- K2 - For not letting me use React in their projects xd.