W
Widgetfied

Embeddable Widgets for Service Businesses

© 2025 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" />

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

Widget disappears on navigation?

  • The hook handles this automatically
  • Make sure you're using the hook in each component
⚡ Quick setup
🚀 Get Started
Come check out the docs here!