JUST DOMJUST-DOM

Create a Plugin

Build your own Just DOM plugin with full type safety

A Just DOM plugin is a plain object with a name and an extend function. The extend function returns an object of methods that get merged onto the DOM object when the consumer calls withPlugins.

Quick Start

import { definePlugin } from "just-dom";

export const greetPlugin = definePlugin({
  name: "greet",
  extend: () => ({
    greeting: (name: string): HTMLDivElement => {
      const el = document.createElement("div");
      el.textContent = `Hello, ${name}!`;
      return el;
    },
  }),
});

That's it. Consumers can now use it:

import DOM, { withPlugins } from "just-dom";
import { greetPlugin } from "./greet-plugin";

const jd = withPlugins(DOM, [greetPlugin]);

const el = jd.greeting("World");
// el is HTMLDivElement, fully typed

In real apps, prefer registering plugins in one place—see App setup (jd.config)—so every file imports the same jd instead of repeating withPlugins.

The Plugin Interface

interface JDPlugin<TExtension> {
  name: string;
  extend: () => TExtension;
}
PropertyTypeDescription
namestringA unique identifier for the plugin
extend() => TExtensionReturns an object of methods to add to the DOM object

The TExtension generic is a Record<string, (...args: any[]) => any> — an object where every value is a function. TypeScript infers the exact shape from your extend return type.

definePlugin

definePlugin is an identity function that provides type inference. It doesn't transform the plugin — it just helps TypeScript infer the extension type correctly.

import { definePlugin } from "just-dom";

// Without definePlugin — you'd need to annotate the type manually
const plugin: JDPlugin<{ myMethod: (x: number) => string }> = {
  name: "manual",
  extend: () => ({
    myMethod: (x: number) => String(x),
  }),
};

// With definePlugin — the type is inferred automatically
const plugin = definePlugin({
  name: "auto",
  extend: () => ({
    myMethod: (x: number) => String(x),
  }),
});

Using Just DOM Internals

Plugins can import and use any export from just-dom:

import { definePlugin, createElement } from "just-dom";
import type { JDCreateElementOptions } from "just-dom";

export const cardPlugin = definePlugin({
  name: "card",
  extend: () => ({
    card: (
      props: JDCreateElementOptions<"div">,
      title: string,
      body: string,
    ): HTMLDivElement => {
      return createElement("div", { ...props, className: "card" }, [
        createElement("h3", {}, [title]),
        createElement("p", {}, [body]),
      ]);
    },
  }),
});

Generic Plugins (Factory Pattern)

For plugins that need configuration, use a factory function. This is how @just-dom/lucide works — the factory accepts a config object and returns a plugin with types derived from that config.

import { definePlugin } from "just-dom";

interface ToastOptions {
  duration?: number;
  position?: "top" | "bottom";
}

export function createToastPlugin(defaults: ToastOptions = {}) {
  return definePlugin({
    name: "toast",
    extend: () => ({
      toast: (message: string, options?: ToastOptions): HTMLDivElement => {
        const merged = { ...defaults, ...options };
        const el = document.createElement("div");
        el.className = `toast toast-${merged.position ?? "bottom"}`;
        el.textContent = message;
        document.body.appendChild(el);

        setTimeout(() => el.remove(), merged.duration ?? 3000);

        return el;
      },
    }),
  });
}

Usage:

import DOM, { withPlugins } from "just-dom";
import { createToastPlugin } from "./toast-plugin";

const jd = withPlugins(DOM, [
  createToastPlugin({ duration: 5000, position: "top" }),
]);

jd.toast("Saved successfully!");

Publishing a Plugin

When publishing a plugin as an npm package:

  1. List just-dom as a peer dependency, not a regular dependency
  2. Build with both CJS and ESM formats (tsup makes this easy)
  3. Include TypeScript declarations (.d.ts / .d.mts)
  4. Use the @just-dom/ scope for official plugins (e.g. @just-dom/toast, @just-dom/lucide)

Application authors usually register published plugins once in App setup (jd.config). New Vite + TypeScript projects can start from create-just-dom.

Example package.json:

{
  "name": "just-dom-my-plugin",
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      "require": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      }
    }
  },
  "peerDependencies": {
    "just-dom": ">=1.0.0"
  }
}

Type Safety

The type system automatically merges plugin extensions onto the DOM type. When using withPlugins, the return type is:

JDom & MergePluginExtensions<Plugins>

This means:

  • All original DOM methods remain fully typed
  • All plugin methods are correctly typed with their parameter and return types
  • TypeScript reports errors if you call a method that doesn't exist
  • Autocompletion works for both built-in and plugin methods

On this page