React & Next.js Integration
Framework GuideDrop Widgetfied widgets into your React app in minutes with our simple component pattern.
The Simplest Way
Just import to your React app & use it like any other component. 1. Copy our hook file 2. Import the widget you need 3. Drop it in your JSX That's it.
Quick Start (2 Minutes)
Copy this hook file, then use widgets like any React component:
1. Create the Hook File
// hooks/useWidgetfied.jsx
import React, { useEffect } from 'react';
// Script loading state (shared globally)
let scriptLoaded = false;
let scriptLoading = false;
function loadScript() {
if (scriptLoaded || scriptLoading) return;
scriptLoading = true;
const script = document.createElement('script');
script.src = 'https://cdn.widgetfied.com/portal.js';
script.async = false;
script.onload = () => {
scriptLoaded = true;
scriptLoading = false;
setTimeout(() => window.Widgetfied?.init?.(), 100);
};
script.onerror = () => {
scriptLoading = false;
};
document.body.appendChild(script);
}
function useWidgetInit() {
useEffect(() => {
if (window.Widgetfied) {
window.Widgetfied.init();
} else {
loadScript();
}
}, []);
}
// Booking Widget Component
export function BookingWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="booking-widget"
data-widget="booking"
data-tenant="YOUR_TENANT_ID"
data-container="booking-widget"
data-display-mode="modal"
className={className}
/>
);
}
// Job Portal Widget Component
export function JobPortalWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="portal-widget"
data-widget="jobportal"
data-tenant="YOUR_TENANT_ID"
data-container="portal-widget"
data-display-mode="modal"
className={className}
/>
);
}
// Estimate Widget Component
export function EstimateWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="estimate-widget"
data-widget="estimate"
data-tenant="YOUR_TENANT_ID"
data-container="estimate-widget"
data-display-mode="modal"
className={className}
/>
);
}
// Payment Widget Component
export function PaymentWidget({ className = '' }) {
useWidgetInit();
return (
<div
id="payment-widget"
data-widget="payment"
data-tenant="YOUR_TENANT_ID"
data-container="payment-widget"
data-display-mode="modal"
className={className}
/>
);
}2. Use It Like Any Component
// pages/booking.jsx
import { BookingWidget } from '../hooks/useWidgetfied';
export default function BookingPage() {
return (
<div className="container mx-auto">
<h1 className="text-2xl font-bold mb-6">Book a Service</h1>
<BookingWidget className="min-h-[400px]" />
</div>
);
}That's It!
The hook handles:
- Script loading (only once)
- Widget initialization
- Lifecycle management
- Multiple widgets on the same page
- Navigation between pages
// Customize with data attributes:
<BookingWidget
className="min-h-[500px] border rounded-lg p-4"
data-display-mode="modal"
/>
// Or create your own variant:
export function MyCustomWidget({ tenantId, displayMode = 'modal', ...props }) {
useWidgetInit();
return (
<div
data-widget="booking"
data-tenant={tenantId}
data-display-mode={displayMode}
data-global-name="myCustomWidget"
className={props.className}
/>
);
}Display Modes
Choose how the widget renders: modal | inline | button
modal ✓Opens as a fullscreen overlay at the document root level. Escapes all CSS constraints.
inlineRenders directly inside the container element. Subject to parent CSS (overflow, transforms, etc).
buttonShows a trigger button. When clicked, opens the widget as a modal overlay.
Use modal or button mode when embedding in sites with deep nesting (10+ layers), overflow: hidden, transform, or backdrop-filter CSS properties. These modes use React portals to render at the document root, escaping any CSS containing blocks.
// Modal mode (default) - opens as overlay, escapes CSS constraints
<BookingWidget data-display-mode="modal" />
// Inline mode - renders in place, affected by parent CSS
<BookingWidget data-display-mode="inline" />
// Button mode - shows button, opens modal on click
<BookingWidget data-display-mode="button" />Advanced Configuration
Global Variable Names
Use data-global-name for multiple widget instances or custom programmatic access:
// Multiple instances with different global names
<BookingWidget data-global-name="bookingWidget1" />
<BookingWidget data-global-name="bookingWidget2" />
// Access programmatically
window.bookingWidget1?.open?.();
window.bookingWidget2?.close?.();Default global name is Widgetfied
All Data Attributes
data-widgetRequired: Widget type (booking, jobportal, etc.)data-tenantRequired: Your tenant IDdata-containerRequired: Unique container identifierdata-display-modeOptional: modal | inline | button (default: modal)data-global-nameOptional: Custom global variable nameLocal Development Setup
For local development, add this proxy to your Vite config:
Required: All these proxy routes must be configured for widgets to work locally.
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api/tenant-config': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/calendar': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/payments': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/portal': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/email': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
},
'/api/tenants': {
target: 'https://www.widgetfied.com',
changeOrigin: true,
secure: true,
}
}
}
})These proxy routes are required for widgets to communicate with Widgetfied APIs during development.
Content Security Policy
If using CSP headers, add these required domains for Widgetfied:
Essential CSP Directives:
// Minimal CSP additions for Widgetfied widgets:
'Content-Security-Policy': `
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.widgetfied.com https://*.widgetfied.com;
connect-src 'self'
https://widgetfied.com https://*.widgetfied.com;
`Full Vite Development CSP Example
// vite.config.js - Complete Development CSP
server: {
headers: {
'Content-Security-Policy': `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.widgetfied.com https://*.widgetfied.com
https://js.stripe.com
https://www.googletagmanager.com
https://www.google-analytics.com;
connect-src 'self' ws: wss:
https://widgetfied.com https://*.widgetfied.com;
style-src 'self' 'unsafe-inline';
frame-src 'self'
https://js.stripe.com
https://hooks.stripe.com;
`
}
}Next.js Setup
Same pattern works in Next.js. Just mark your component as client:
// app/booking/page.js
'use client';
import { BookingWidget } from '@/hooks/useWidgetfied';
export default function BookingPage() {
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Schedule Your Service</h1>
<BookingWidget className="min-h-[500px]" />
</div>
);
}Required: Add this configuration to your next.config.js for widgets to work:
// next.config.js - Required API Proxy & CSP
module.exports = {
async rewrites() {
return [
{
source: '/api/tenant-config/:path*',
destination: 'https://www.widgetfied.com/api/tenant-config/:path*',
},
{
source: '/api/calendar/:path*',
destination: 'https://www.widgetfied.com/api/calendar/:path*',
},
{
source: '/api/payments/:path*',
destination: 'https://www.widgetfied.com/api/payments/:path*',
},
{
source: '/api/portal/:path*',
destination: 'https://www.widgetfied.com/api/portal/:path*',
},
{
source: '/api/email/:path*',
destination: 'https://www.widgetfied.com/api/email/:path*',
},
{
source: '/api/tenants/:path*',
destination: 'https://www.widgetfied.com/api/tenants/:path*',
}
];
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: `
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.widgetfied.com https://*.widgetfied.com
https://js.stripe.com;
connect-src 'self'
https://widgetfied.com https://*.widgetfied.com;
`.replace(/\s+/g, ' ').trim()
}
]
}
];
}
};Common Issues
Widget not showing?
- →Check your tenant ID is correct
- →Make sure the container has a minimum height
- →Verify the script loaded (check Network tab)
CORS or API errors?
- →Add the proxy config to your dev server
- →Check your tenant is active in the dashboard
Widget disappears on navigation?
- →The hook handles this automatically
- →Make sure you're using the hook in each component