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:
Unique identifier for the plugin
Partial theme overrides to customize the visual appearance
Override default options like activationMode, keyHoldDuration, maxContextLines, etc.
Array of context menu actions and/or toolbar menu items
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. The React Grab API instance with methods like activate(), deactivate(), copyElement(), getSource(), etc.
Action context hooks for transforming content and handling file operations
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
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:
Called when React Grab is activated
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
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:
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
Unique Names : Always use unique plugin names to avoid conflicts
Cleanup : Return a cleanup function from setup() to clean up resources
Type Safety : Use TypeScript interfaces for better development experience
Error Handling : Handle errors gracefully in async operations
Performance : Avoid heavy operations in frequently called hooks like onElementHover
Unregister : Always unregister plugins when components unmount in React
See Also