widgetfied

© 2026 Widgetfied

React & Next.js Integration

Framework Guide

Drop 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

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

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.

inline

Renders directly inside the container element. Subject to parent CSS (overflow, transforms, etc).

button

Shows 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" />

SPA Navigation (Important)

In Single Page Applications (React Router, Next.js App Router, etc.), route changes don't trigger a full page reload. The Widgetfied script loads once, but when React unmounts and remounts widget containers during navigation, the script must re-scan the DOM to pick up new containers.

The Problem: If you use useEffect with an empty dependency array [], the init call only runs on first mount. Navigating away and back causes widgets to disappear because Widgetfied doesn't know about the newly-mounted containers.

Solution: Remove the dependency array

Let the effect run on every render so Widgetfied.init() is called whenever a widget component mounts after SPA navigation:

// SPA-safe hook — re-initializes on every mount/navigation
function useWidgetInit() {
  const mountedRef = useRef(false);

  const initWidget = useCallback(() => {
    if (window.Widgetfied?.init) {
      window.Widgetfied.init();
    }
  }, []);

  useEffect(() => {
    mountedRef.current = true;

    if (scriptLoaded && window.Widgetfied) {
      // Script loaded — re-init after a tick so DOM container is painted
      const timer = setTimeout(initWidget, 50);
      return () => { mountedRef.current = false; clearTimeout(timer); };
    }

    // Script not ready — load it, init when done
    loadScript(() => {
      if (mountedRef.current) setTimeout(initWidget, 50);
    });

    return () => { mountedRef.current = false; };
  }); // ← No dependency array! Runs on every render/mount
}

This is safe because Widgetfied.init() is idempotent — calling it multiple times only processes uninitialized containers. The 50ms delay ensures the DOM element is fully painted before scanning.

Bonus: Prefetch the script in your HTML

Add these to your index.html <head> so the browser starts fetching before React even mounts:

<!-- Preload widget script + DNS prefetch for API calls -->
<link rel="preload" href="https://cdn.widgetfied.com/portal.js" as="script" crossorigin />
<link rel="dns-prefetch" href="https://cdn.widgetfied.com" />
<link rel="dns-prefetch" href="https://www.widgetfied.com" />

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 ID
data-containerRequired: Unique container identifier
data-display-modeOptional: modal | inline | button (default: modal)
data-global-nameOptional: Custom global variable name

Local 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

Widgets disappear after SPA navigation?

  • Remove the empty [] dependency array from useEffect — see "SPA Navigation" section above
  • Ensure useWidgetInit() runs on every render so Widgetfied.init() re-scans for new containers
  • Add <link rel="preload"> in your HTML <head> for faster script availability
⚡ Quick setup
🚀 Get Started
DOCS