Skip to main content

Overview

React Grab’s plugin system allows you to extend functionality with custom context menu actions, toolbar items, lifecycle hooks, and theme overrides. Plugins provide a powerful way to integrate React Grab into your development workflow.

Plugin Interface

A plugin is an object that implements the Plugin interface:
name
string
required
Unique identifier for the plugin
theme
DeepPartial<Theme>
Partial theme overrides to customize the visual appearance
options
SettableOptions
Override default options like activationMode, keyHoldDuration, maxContextLines, etc.
actions
PluginAction[]
Array of context menu actions and/or toolbar menu items
hooks
PluginHooks
Lifecycle callbacks for events like activation, element selection, copying, etc.
setup
(api: ReactGrabAPI, hooks: ActionContextHooks) => PluginConfig | void
Setup function called when plugin is registered. Receives the full ReactGrabAPI and can return additional configuration or a cleanup function.

Registering a Plugin

Simple Registration

Register a plugin directly on the global API:
window.__REACT_GRAB__.registerPlugin({
  name: "my-plugin",
  hooks: {
    onElementSelect: (element) => {
      console.log("Selected:", element.tagName);
    },
  },
});

React Component Registration

Register inside a useEffect to ensure React Grab is loaded:
import { useEffect } from "react";

export function MyPlugin() {
  useEffect(() => {
    const api = window.__REACT_GRAB__;
    if (!api) return;

    api.registerPlugin({
      name: "my-plugin",
      actions: [
        {
          id: "my-action",
          label: "My Action",
          shortcut: "M",
          onAction: (context) => {
            console.log("Action on:", context.element);
            context.hideContextMenu();
          },
        },
      ],
    });

    return () => api.unregisterPlugin("my-plugin");
  }, []);

  return null;
}

Built-in Plugin Examples

Comment Plugin

The comment plugin adds context menu and toolbar actions for entering prompt mode:
import type { Plugin } from "react-grab";

export const commentPlugin: Plugin = {
  name: "comment",
  setup: (api) => ({
    actions: [
      {
        id: "comment",
        label: "Comment",
        shortcut: "Enter",
        onAction: (context) => {
          context.enterPromptMode?.();
        },
      },
      {
        id: "comment-toolbar",
        label: "Comment",
        shortcut: "Enter",
        target: "toolbar",
        onAction: () => {
          api.comment();
        },
      },
    ],
  }),
};

Open Plugin

The open plugin allows opening source files in your editor:
import type { Plugin } from "react-grab";
import { openFile } from "./utils/open-file";

export const openPlugin: Plugin = {
  name: "open",
  actions: [
    {
      id: "open",
      label: "Open",
      shortcut: "O",
      enabled: (context) => Boolean(context.filePath),
      onAction: (context) => {
        if (!context.filePath) return;

        const wasHandled = context.hooks.onOpenFile(
          context.filePath,
          context.lineNumber,
        );

        if (!wasHandled) {
          openFile(
            context.filePath,
            context.lineNumber,
            context.hooks.transformOpenFileUrl,
          );
        }

        context.hideContextMenu();
        context.cleanup();
      },
    },
  ],
};

Copy HTML Plugin

A more complex plugin that uses both hooks and actions:
import type { Plugin } from "react-grab";
import { appendStackContext } from "./utils/append-stack-context";
import { copyContent } from "./utils/copy-content";

export const copyHtmlPlugin: Plugin = {
  name: "copy-html",
  setup: (api, hooks) => {
    let isPendingSelection = false;

    return {
      hooks: {
        onElementSelect: (element) => {
          if (!isPendingSelection) return;
          isPendingSelection = false;
          
          void Promise.all([
            hooks.transformHtmlContent(element.outerHTML, [element]),
            api.getStackContext(element),
          ])
            .then(([transformedHtml, stackContext]) => {
              if (!transformedHtml) return;
              copyContent(appendStackContext(transformedHtml, stackContext));
            })
            .catch(() => {});
          
          return true;
        },
        onDeactivate: () => {
          isPendingSelection = false;
        },
        cancelPendingToolbarActions: () => {
          isPendingSelection = false;
        },
      },
      actions: [
        {
          id: "copy-html",
          label: "Copy HTML",
          onAction: async (context) => {
            await context.performWithFeedback(async () => {
              const combinedHtml = context.elements
                .map((element) => element.outerHTML)
                .join("\n\n");

              const transformedHtml = await context.hooks.transformHtmlContent(
                combinedHtml,
                context.elements,
              );

              if (!transformedHtml) return false;

              const stackContext = await api.getStackContext(context.element);
              return copyContent(
                appendStackContext(transformedHtml, stackContext),
                {
                  componentName: context.componentName,
                  tagName: context.tagName,
                },
              );
            });
          },
        },
        {
          id: "copy-html-toolbar",
          label: "Copy HTML",
          target: "toolbar",
          onAction: () => {
            isPendingSelection = true;
            api.activate();
          },
        },
      ],
    };
  },
};

Plugin Hooks

Plugins can listen to various lifecycle events:
onActivate
() => void
Called when React Grab is activated
onDeactivate
() => void
Called when React Grab is deactivated
onElementHover
(element: Element) => void
Called when hovering over an element
onElementSelect
(element: Element) => boolean | void | Promise<boolean>
Called when an element is selected. Return true to prevent default selection behavior.
onBeforeCopy
(elements: Element[]) => void | Promise<void>
Called before copying elements to clipboard
transformCopyContent
(content: string, elements: Element[]) => string | Promise<string>
Transform the content before it’s copied to clipboard
onAfterCopy
(elements: Element[], success: boolean) => void
Called after copy operation completes
onCopySuccess
(elements: Element[], content: string) => void
Called when copy succeeds
onCopyError
(error: Error) => void
Called when copy fails
transformHtmlContent
(html: string, elements: Element[]) => string | Promise<string>
Transform HTML content before copying
transformAgentContext
(context: AgentContext, elements: Element[]) => AgentContext | Promise<AgentContext>
Transform the context passed to AI agents
onOpenFile
(filePath: string, lineNumber?: number) => boolean | void
Handle file opening. Return true to prevent default behavior.
transformOpenFileUrl
(url: string, filePath: string, lineNumber?: number) => string
Transform the URL used to open files in editor

PluginConfig

The setup function can return a PluginConfig object:
theme
DeepPartial<Theme>
Theme overrides
options
SettableOptions
Option overrides
actions
PluginAction[]
Actions to register
hooks
PluginHooks
Lifecycle hooks
cleanup
() => void
Cleanup function called when the plugin is unregistered

Complete Example

Here’s a complete plugin that adds a custom analytics tracker:
import type { Plugin } from "react-grab";

const analyticsPlugin: Plugin = {
  name: "analytics",
  setup: (api) => {
    const trackEvent = (event: string, data: Record<string, any>) => {
      console.log("Analytics:", event, data);
      // Send to your analytics service
    };

    return {
      hooks: {
        onActivate: () => {
          trackEvent("grab_activated", {});
        },
        onElementSelect: (element) => {
          trackEvent("element_selected", {
            tagName: element.tagName,
          });
        },
        onCopySuccess: (elements, content) => {
          trackEvent("copy_success", {
            elementCount: elements.length,
            contentLength: content.length,
          });
        },
      },
      actions: [
        {
          id: "track-element",
          label: "Track Element",
          shortcut: "T",
          onAction: (context) => {
            trackEvent("custom_track", {
              tagName: context.tagName,
              componentName: context.componentName,
              filePath: context.filePath,
            });
            context.hideContextMenu();
          },
        },
      ],
      cleanup: () => {
        trackEvent("plugin_unregistered", {});
      },
    };
  },
};

// Register the plugin
window.__REACT_GRAB__.registerPlugin(analyticsPlugin);

Best Practices

  1. Unique Names: Always use unique plugin names to avoid conflicts
  2. Cleanup: Return a cleanup function from setup() to clean up resources
  3. Type Safety: Use TypeScript interfaces for better development experience
  4. Error Handling: Handle errors gracefully in async operations
  5. Performance: Avoid heavy operations in frequently called hooks like onElementHover
  6. Unregister: Always unregister plugins when components unmount in React

See Also

Build docs developers (and LLMs) love