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 typedIn 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;
}| Property | Type | Description |
|---|---|---|
name | string | A unique identifier for the plugin |
extend | () => TExtension | Returns 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:
- List
just-domas a peer dependency, not a regular dependency - Build with both CJS and ESM formats (tsup makes this easy)
- Include TypeScript declarations (
.d.ts/.d.mts) - 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
DOMmethods 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