React
Shadcn/UI
Radix
Lucide-react
Plugins
UI/UX

Developing Plugins for React with Shadcn/UI, Radix and Lucide-react: Complete Guide

Developing Plugins for React with Shadcn/UI, Radix and Lucide-react: Complete Guide

By 2025, the React ecosystem has evolved significantly, with Shadcn/UI and Radix establishing themselves as de facto standards for developing modern, accessible interfaces. This guide explores best practices for developing custom plugins that integrate seamlessly with this ecosystem.

Fundamentals and Architecture

Recommended Plugin Structure

// src/plugins/MyCustomPlugin/index.ts import { createPlugin } from '@shadcn/plugin-core'; import { Root, Trigger, Content } from '@radix-ui/react-dialog'; import { Settings2 } from 'lucide-react'; export interface MyCustomPluginProps { theme?: 'light' | 'dark'; position?: 'left' | 'right'; onStateChange?: (state: PluginState) => void; } export const MyCustomPlugin = createPlugin<MyCustomPluginProps>({ name: 'my-custom-plugin', version: '1.0.0', setup(props) { return { components: { Root, Trigger, Content, }, icons: { Settings: Settings2, }, theme: props.theme || 'light', }; }, });

Strong Type System

// src/types/plugin.ts export interface PluginConfig { name: string; version: string; dependencies?: Record<string, string>; peerDependencies?: Record<string, string>; } export interface PluginContext<T = unknown> { theme: 'light' | 'dark'; components: Record<string, React.ComponentType>; icons: Record<string, LucideIcon>; state: T; } export interface PluginHooks<T = unknown> { useState(): [T, (value: T) => void]; useEffect(effect: () => void, deps?: any[]): void; useContext(): PluginContext<T>; }

Integrating with Shadcn/UI

Custom Base Component

// src/components/CustomComponent.tsx import * as React from 'react'; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Dialog } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; interface CustomComponentProps { className?: string; children?: React.ReactNode; onAction?: () => void; } export const CustomComponent = React.forwardRef< HTMLDivElement, CustomComponentProps >(({ className, children, onAction, ...props }, ref) => { const [open, setOpen] = React.useState(false); return ( <div ref={ref} className={cn( "flex flex-col space-y-4 rounded-lg border p-4", className )} {...props} > <Dialog open={open} onOpenChange={setOpen}> <Dialog.Trigger asChild> <Button variant="outline">Abrir</Button> </Dialog.Trigger> <Dialog.Content> <Dialog.Header> <Dialog.Title>Configurações do Plugin</Dialog.Title> <Dialog.Description> Ajuste as configurações do seu plugin aqui. </Dialog.Description> </Dialog.Header> <div className="grid gap-4 py-4"> <Input id="config" placeholder="Configuração" className="col-span-3" /> </div> <Dialog.Footer> <Button onClick={onAction}>Salvar</Button> </Dialog.Footer> </Dialog.Content> </Dialog> {children} </div> ); }); CustomComponent.displayName = "CustomComponent";

Custom Hooks

// src/hooks/usePluginState.ts import { create } from 'zustand'; interface PluginState { isEnabled: boolean; config: Record<string, unknown>; theme: 'light' | 'dark'; } export const usePluginState = create<PluginState>((set) => ({ isEnabled: false, config: {}, theme: 'light', toggleEnabled: () => set((state) => ({ isEnabled: !state.isEnabled })), updateConfig: (config: Partial<Record<string, unknown>>) => set((state) => ({ config: { ...state.config, ...config }, })), setTheme: (theme: 'light' | 'dark') => set({ theme }), }));

Integrating with Radix Primitives

Accessible Components

// src/components/AccessiblePlugin.tsx import * as React from 'react'; import * as Tooltip from '@radix-ui/react-tooltip'; import * as Switch from '@radix-ui/react-switch'; import { styled } from '@stitches/react'; const StyledSwitch = styled(Switch.Root, { width: 42, height: 25, backgroundColor: 'var(--black-a9)', borderRadius: '9999px', position: 'relative', '&[data-state="checked"]': { backgroundColor: 'var(--primary)', }, }); const StyledThumb = styled(Switch.Thumb, { width: 21, height: 21, backgroundColor: 'white', borderRadius: '9999px', transition: 'transform 100ms', transform: 'translateX(2px)', '&[data-state="checked"]': { transform: 'translateX(19px)', }, }); export const AccessiblePlugin: React.FC = () => { const [enabled, setEnabled] = React.useState(false); return ( <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <StyledSwitch checked={enabled} onCheckedChange={setEnabled} aria-label="Toggle plugin" > <StyledThumb /> </StyledSwitch> </Tooltip.Trigger> <Tooltip.Portal> <Tooltip.Content className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground animate-in fade-in-0 zoom-in-95" sideOffset={5} > {enabled ? 'Desativar' : 'Ativar'} plugin <Tooltip.Arrow className="fill-primary" /> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> </Tooltip.Provider> ); };

Integrating with Lucide-react

Custom Icon System

// src/components/IconSystem.tsx import * as React from 'react'; import { Settings, Plus, Minus, Check, X, ChevronRight, ChevronLeft, ChevronUp, ChevronDown, } from 'lucide-react'; export const iconMap = { settings: Settings, plus: Plus, minus: Minus, check: Check, close: X, chevronRight: ChevronRight, chevronLeft: ChevronLeft, chevronUp: ChevronUp, chevronDown: ChevronDown, } as const; interface IconProps { name: keyof typeof iconMap; size?: number; className?: string; } export const Icon: React.FC<IconProps> = ({ name, size = 24, className, }) => { const IconComponent = iconMap[name]; return <IconComponent size={size} className={className} />; };

Best Practices

1. State Management

// src/store/pluginStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface PluginStore { plugins: Record<string, boolean>; settings: Record<string, unknown>; togglePlugin: (id: string) => void; updateSettings: (id: string, settings: unknown) => void; } export const usePluginStore = create<PluginStore>()( persist( (set) => ({ plugins: {}, settings: {}, togglePlugin: (id) => set((state) => ({ plugins: { ...state.plugins, [id]: !state.plugins[id], }, })), updateSettings: (id, settings) => set((state) => ({ settings: { ...state.settings, [id]: settings, }, })), }), { name: 'plugin-store', } ) );

2. Theme System

// src/themes/plugin-theme.ts import { createTheme } from '@shadcn/theme'; export const lightTheme = createTheme({ colors: { primary: 'hsl(222.2 47.4% 11.2%)', secondary: 'hsl(217.2 32.6% 17.5%)', accent: 'hsl(210 40% 96.1%)', background: 'hsl(0 0% 100%)', foreground: 'hsl(222.2 47.4% 11.2%)', }, }); export const darkTheme = createTheme({ colors: { primary: 'hsl(210 40% 98%)', secondary: 'hsl(217.2 32.6% 17.5%)', accent: 'hsl(217.2 32.6% 17.5%)', background: 'hsl(222.2 47.4% 11.2%)', foreground: 'hsl(210 40% 98%)', }, });

3. Event System

// src/events/plugin-events.ts type EventCallback = (...args: any[]) => void; class PluginEventSystem { private events: Map<string, Set<EventCallback>>; constructor() { this.events = new Map(); } on(event: string, callback: EventCallback) { if (!this.events.has(event)) { this.events.set(event, new Set()); } this.events.get(event)!.add(callback); } off(event: string, callback: EventCallback) { this.events.get(event)?.delete(callback); } emit(event: string, ...args: any[]) { this.events.get(event)?.forEach((callback) => { callback(...args); }); } } export const eventSystem = new PluginEventSystem();

Complete Plugin Example

// src/plugins/DataTablePlugin/index.tsx import * as React from 'react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { MoreHorizontal, ArrowUpDown } from 'lucide-react'; interface DataTablePluginProps<T> { data: T[]; columns: { key: keyof T; label: string; sortable?: boolean; }[]; onSort?: (key: keyof T) => void; } export function DataTablePlugin<T>({ data, columns, onSort, }: DataTablePluginProps<T>) { const [sortKey, setSortKey] = React.useState<keyof T | null>(null); const [searchTerm, setSearchTerm] = React.useState(""); const handleSort = (key: keyof T) => { setSortKey(key); onSort?.(key); }; const filteredData = React.useMemo(() => { if (!searchTerm) return data; return data.filter((item) => Object.values(item).some((value) => String(value) .toLowerCase() .includes(searchTerm.toLowerCase()) ) ); }, [data, searchTerm]); return ( <div className="space-y-4"> <Input placeholder="Pesquisar..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="max-w-sm" /> <Table> <TableHeader> <TableRow> {columns.map((column) => ( <TableHead key={String(column.key)}> <div className="flex items-center space-x-2"> <span>{column.label}</span> {column.sortable && ( <Button variant="ghost" size="sm" onClick={() => handleSort(column.key)} > <ArrowUpDown className="h-4 w-4" /> </Button> )} </div> </TableHead> ))} <TableHead className="w-[100px]">Ações</TableHead> </TableRow> </TableHeader> <TableBody> {filteredData.map((row, i) => ( <TableRow key={i}> {columns.map((column) => ( <TableCell key={String(column.key)}> {String(row[column.key])} </TableCell> ))} <TableCell> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0" > <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem> Editar </DropdownMenuItem> <DropdownMenuItem> Excluir </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </TableCell> </TableRow> ))} </TableBody> </Table> </div> ); }

Tests and Quality

1. Unit Tests

// src/plugins/__tests__/DataTablePlugin.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { DataTablePlugin } from '../DataTablePlugin'; describe('DataTablePlugin', () => { const mockData = [ { id: 1, name: 'John', age: 30 }, { id: 2, name: 'Jane', age: 25 }, ]; const columns = [ { key: 'name', label: 'Nome', sortable: true }, { key: 'age', label: 'Idade', sortable: true }, ]; it('renders all columns and rows', () => { render( <DataTablePlugin data={mockData} columns={columns} /> ); expect(screen.getByText('Nome')).toBeInTheDocument(); expect(screen.getByText('Idade')).toBeInTheDocument(); expect(screen.getByText('John')).toBeInTheDocument(); expect(screen.getByText('30')).toBeInTheDocument(); }); it('filters data based on search term', () => { render( <DataTablePlugin data={mockData} columns={columns} /> ); const searchInput = screen.getByPlaceholderText('Pesquisar...'); fireEvent.change(searchInput, { target: { value: 'John' } }); expect(screen.getByText('John')).toBeInTheDocument(); expect(screen.queryByText('Jane')).not.toBeInTheDocument(); }); });

2. Integration Tests

// src/plugins/__tests__/integration.test.tsx import { render, act } from '@testing-library/react'; import { MyCustomPlugin } from '../MyCustomPlugin'; import { usePluginStore } from '../../store/pluginStore'; describe('Plugin Integration', () => { beforeEach(() => { usePluginStore.setState({ plugins: {}, settings: {}, }); }); it('integrates with plugin store', () => { render(<MyCustomPlugin />); act(() => { usePluginStore.getState().togglePlugin('my-custom-plugin'); }); expect( usePluginStore.getState().plugins['my-custom-plugin'] ).toBe(true); }); });

Implementation Checklist

Before Development

  • Define clear plugin requirements
  • Identify necessary dependencies
  • Plan architecture and components
  • Establish code standards

During Development

  • Follow SOLID principles
  • Implement unit tests
  • Document functions and components
  • Maintain consistency with Shadcn/UI

Post-Development

  • Run integration tests
  • Check accessibility
  • Optimize performance
  • Prepare documentation

Conclusion

Developing plugins for React using Shadcn/UI, Radix and Lucide-react requires a structured approach and attention to detail. Best practices include:

  1. Modular Architecture: Reusable and well-organized components
  2. Strong Typing: TypeScript for greater security
  3. Accessibility: Radix components to ensure accessibility
  4. Visual Consistency: Integration with Shadcn/UI and Lucide-react
  5. Testability: Adequate test coverage

Next Steps

  1. Explore the examples provided
  2. Adapt the patterns to your project
  3. Contribute to the community
  4. Stay up to date with ecosystem developments

Are you developing plugins for React? Share your experiences and questions in the comments below!

Also read