Dev ToolbarIntegrationsLibrary Integration

Library Integration

Add zero-dependency DevTools support to your library. Users get devtools automatically when they install the Visulima Dev Toolbar — no extra configuration required.

Last updated:

If you are building a library (a state manager, data fetcher, router, etc.), you can integrate with the Visulima Dev Toolbar without taking a hard dependency on @visulima/dev-toolbar. Your library checks for the global hook at runtime — when the toolbar is present, devtools appear; when it is absent, nothing happens.

This is the same pattern used by React Query DevTools, Pinia DevTools, and Vue Router DevTools.

The Global Hook

The toolbar exposes window.__DEV_TOOLBAR_HOOK__ as soon as it initializes. Libraries can interact with it to:

  • Register a custom app panel
  • Add events to the Timeline
  • Subscribe to toolbar lifecycle events

The hook is buffered — calls made before the toolbar has mounted are queued and replayed once it initializes. This means you can safely call __DEV_TOOLBAR_HOOK__ as soon as your library starts, without worrying about load order.


Quick Integration

The minimum integration registers an app and emits timeline events:

my-library/src/devtools.ts
function initDevTools(myLibraryInstance: MyLibrary): void {
    if (typeof window === "undefined") return;

    const hook = (window as any).__DEV_TOOLBAR_HOOK__;

    if (!hook) return;

    // Register a devtools panel for your library
    hook.registerApp({
        id: "my-library:devtools",
        name: "My Library",
        icon: myLibraryIconSvg, // raw SVG string
        component: MyLibraryPanel, // Preact component
        tooltip: MyLibraryTooltip, // optional
    });

    // Emit timeline events as things happen
    myLibraryInstance.on("query:start", (query) => {
        hook.addTimelineEvent("my-library", {
            id: crypto.randomUUID(),
            title: `Query: ${query.name}`,
            time: Date.now(),
            level: "info",
            data: query,
        });
    });
}

Call this function from your library's dev-mode initialization:

my-library/src/index.ts
export function createMyLibrary(options: Options): MyLibrary {
    const instance = new MyLibrary(options);

    // Only initialize devtools in development
    if (process.env.NODE_ENV !== "production") {
        initDevTools(instance);
    }

    return instance;
}

Full Step-by-Step Guide

Create the Devtools Panel Component

Build a Preact component for your library's devtools panel. The component receives helpers.rpc for any server-side needs, but library devtools often only need in-memory state:

my-library/src/devtools/panel.tsx
/** @jsxImportSource preact */
import type { ComponentChildren } from "preact";
import { useEffect, useState } from "preact/hooks";
import type { AppComponentProps } from "@visulima/dev-toolbar";
import { myLibraryStore } from "../store";

const MyLibraryPanel = (_props: AppComponentProps): ComponentChildren => {
    const [state, setState] = useState(myLibraryStore.getSnapshot());

    useEffect(() => {
        return myLibraryStore.subscribe(() => {
            setState(myLibraryStore.getSnapshot());
        });
    }, []);

    return (
        <div class="p-5 space-y-4">
            <h2 class="text-xs font-bold uppercase tracking-widest text-muted-foreground">// My Library State</h2>
            <pre class="text-xs font-mono text-foreground/80 bg-foreground/5 border border-border p-4 overflow-auto">{JSON.stringify(state, null, 2)}</pre>
        </div>
    );
};

export default MyLibraryPanel;

(Optional) Create a Tooltip

A tooltip shows a compact summary on hover without opening the full panel:

my-library/src/devtools/tooltip.tsx
/** @jsxImportSource preact */
import type { ComponentChildren } from "preact";
import { useEffect, useState } from "preact/hooks";
import type { AppComponentProps } from "@visulima/dev-toolbar";
import { myLibraryStore } from "../store";

const MyLibraryTooltip = (_props: AppComponentProps): ComponentChildren => {
    const [count, setCount] = useState(myLibraryStore.getCount());

    useEffect(() => {
        return myLibraryStore.subscribe(() => {
            setCount(myLibraryStore.getCount());
        });
    }, []);

    return (
        <div class="p-3 space-y-1 min-w-32">
            <div class="text-xs font-bold text-muted-foreground uppercase tracking-widest">My Library</div>
            <div class="text-sm font-mono text-foreground">{count} items tracked</div>
        </div>
    );
};

export default MyLibraryTooltip;

Register with the Hook

my-library/src/devtools/index.ts
import myIcon from "./icon.svg?raw";
import MyLibraryPanel from "./panel";
import MyLibraryTooltip from "./tooltip";
import type { MyLibrary } from "../types";

export function installDevtools(instance: MyLibrary): void {
    if (typeof window === "undefined") return;

    const hook = (window as any).__DEV_TOOLBAR_HOOK__;

    if (!hook) return;

    // Register the app panel
    hook.registerApp({
        id: "my-library:devtools",
        name: "My Library",
        icon: myIcon,
        component: MyLibraryPanel,
        tooltip: MyLibraryTooltip,
    });

    // Emit timeline events
    instance.on("action", (action) => {
        hook.addTimelineEvent("my-library", {
            id: crypto.randomUUID(),
            title: action.type,
            subtitle: action.payload ? JSON.stringify(action.payload).slice(0, 40) : undefined,
            time: Date.now(),
            level: "info",
            data: action,
        });
    });

    // Subscribe to toolbar events
    hook.on("devtools:init", () => {
        console.log("[my-library] DevTools initialized");
    });
}

Call from Your Library Entry Point

my-library/src/index.ts
import { installDevtools } from "./devtools";

export function createMyLibrary(options: Options): MyLibrary {
    const instance = new MyLibrary(options);

    if (process.env.NODE_ENV !== "production") {
        installDevtools(instance);
    }

    return instance;
}

Hook API Reference

interface DevToolbarHook {
    on(event: string, handler: (...args: any[]) => void): () => void;
    once(event: string, handler: (...args: any[]) => void): void;
    off(event: string, handler?: (...args: any[]) => void): void;
    emit(event: string, ...args: any[]): void;
    registerApp(app: DevToolbarApp): void;
    addTimelineEvent(groupId: string, event: TimelineEvent): void;
}

Hook Events

EventPayloadDescription
devtools:initToolbar has mounted and is ready
devtools:openappId: stringAn app panel was opened
devtools:closeThe panel was closed
app:error(error: Error, appId?: string)An app component threw an error
timeline:event(groupId, event)A timeline event was added

Timeline Events

Use timeline events to give users a chronological view of your library's activity:

hook.addTimelineEvent("my-library", {
    id: crypto.randomUUID(),
    title: "Cache invalidated",
    subtitle: "/api/users",
    time: Date.now(),
    level: "warning",
    data: {
        reason: "stale-while-revalidate",
        age: 31000,
    },
});

Timeline groups are automatically created from the groupId string. Users can filter events by group in the Timeline app.


No Dependency Required

Your library does not need to add @visulima/dev-toolbar as a dependency. The integration is entirely runtime-based:

// ✅ No import from @visulima/dev-toolbar
const hook = (window as any).__DEV_TOOLBAR_HOOK__;
if (hook) {
    /* ... */
}

If your library ships TypeScript types and you want to type the hook, use a declare global:

my-library/src/devtools/types.ts
declare global {
    interface Window {
        __DEV_TOOLBAR_HOOK__?: {
            registerApp(app: { id: string; name: string; icon: string; component: unknown; tooltip?: unknown }): void;
            addTimelineEvent(
                groupId: string,
                event: {
                    id: string;
                    title: string;
                    time: number;
                    level?: "info" | "warning" | "error";
                    data?: Record<string, unknown>;
                },
            ): void;
            on(event: string, handler: (...args: any[]) => void): () => void;
        };
    }
}

The hook is initialized before user code runs. The toolbar script is injected into <head> with prepend priority. Even so, calls before devtools:init are safely buffered and replayed.


Real-World Pattern

Here is a complete minimal integration modeled on how popular libraries wire up devtools:

my-library/src/devtools.ts
let installed = false;

export function tryInstallDevTools(instance: MyLibrary): void {
    // Avoid double-installing (e.g. if createMyLibrary is called twice)
    if (installed) return;
    if (typeof window === "undefined") return;
    if (process.env.NODE_ENV === "production") return;

    const hook = (window as any).__DEV_TOOLBAR_HOOK__;
    if (!hook) return;

    installed = true;

    hook.registerApp({
        id: "my-library:panel",
        name: "My Library",
        icon: ICON_SVG,
        component: Panel,
        tooltip: Tooltip,
    });

    // Timeline integration
    instance.subscribe((event) => {
        hook.addTimelineEvent("my-library", {
            id: crypto.randomUUID(),
            title: event.type,
            time: Date.now(),
            level: event.error ? "error" : "info",
            data: event,
        });
    });

    // Badge notification when errors occur
    instance.on("error", () => {
        const api = (window as any).__VISULIMA_DEVTOOLS__;
        api?.notify("my-library:panel", "error");
    });
}
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues