Skip to content

Repository files navigation

<Paradox/>

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

GitHub contributors GitHub commit activity GitHub issues GitHub pull requests

IMPORTANT: Things are changing a lot right now, so please be patient, this is a work in progress

Table of Contents

About The Project

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

Getting Started

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

Requirements

  • Node.js >= v16.16.0 (For development mode)

Installation

  1. Clone the repo
    git clone git@github.com:rincorpes/paradoxjs.git
  2. Install NPM packages
    cd paradoxjs
    npm install

Usage

Paradox is meant to be adopted incrementally. Use the parts that help, ignore the rest.

Paradox from npm

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/paradoxjs

Then you can import it like this:

import Paradox from "@rincorpes/paradoxjs";

Now you'll have access to Paradox in your project.

Project structure example

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 src folder contains the app entry point and styles where you can import Paradox.
  • The dist folder contains the production build generated by Vite.
  • The index.html file is the HTML entry used by Vite during development and build.
  • The node_modules folder contains the npm packages for the project.
  • The package.json file contains the project info and the npm scripts.
  • The package-lock.json file contains the npm packages info.
  • The vite.config.js file contains the Vite config.

Vite config

import { defineConfig } from "vite";

export default defineConfig({
  server: {
    host: "localhost",
    port: 3040,
  },
  preview: {
    host: "localhost",
    port: 3040,
  },
});

Paradox in plain HTML projects

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.

API Stability

Paradox currently uses three API stability tiers.

Stable

These are the supported public APIs for this iteration:

  • Paradox.buildElement
  • Paradox.delegate
  • Paradox.Router
  • Paradox.pubsub
  • The default Paradox export from @rincorpes/paradoxjs

These utilities reflect the main direction of the library as a vanilla JavaScript utility belt.

Experimental

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.

Internal

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/* and core/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";

TypeScript Note

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

Documentation

Paradox currently includes the following public utilities:

Build an element with Paradox.buildElement

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 buildElement function 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.
  • data and aria keys are translated into data-* and aria-* attributes automatically.

Enhance existing HTML with Paradox.delegate

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:

  • delegate works well with static HTML and data-* 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.

Routes with Paradox.Router

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=Paradox
  • query: A URLSearchParams instance for the current URL
  • params: A Map of dynamic route params, like id from /users/:id
  • baseUrl: The router base URL
  • path: The matched path
  • route: The matched route definition
  • router: 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 #/path hash, Paradox will still resolve it on load for backward compatibility, but regular path-based URLs are the preferred mode for this iteration.

PubSub with Paradox.pubsub

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.unsubscribe method to unsubscribe from an event.
  • You can not publish an event before subscribing to it xd.

Examples

You can find some examples in the examples folder.

Contributing

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.

License

Distributed under the MIT License. See LICENSE for more information.

Contact

Santiago Rincon - @alexsc6955

Roadmap

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.

Acknowledgements

  • K2 - For not letting me use React in their projects xd.

About

Lightweight UI and DOM utilities for browser-side JavaScript.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages