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:
- Modular Architecture: Reusable and well-organized components
- Strong Typing: TypeScript for greater security
- Accessibility: Radix components to ensure accessibility
- Visual Consistency: Integration with Shadcn/UI and Lucide-react
- Testability: Adequate test coverage
Next Steps
- Explore the examples provided
- Adapt the patterns to your project
- Contribute to the community
- Stay up to date with ecosystem developments
Are you developing plugins for React? Share your experiences and questions in the comments below!
Also read
- Modern Design Systems: Building Scalable Component Libraries with React and TypeScript
- Internationalization Best Practices (i18n) in React and Next.js in 2025
- Next.js App Router: The Guide to Thinking Server by Default
- What Are React Server Components and Why Logic is Moving Back to the Server
- Cache and streaming in Next.js: performance became an architectural decision
- Micro-frontends: When and Why to Adopt in Scalable Projects
