Documentation Index
Fetch the complete documentation index at: https://docs.fanfare.io/llms.txt
Use this file to discover all available pages before exploring further.
Custom to Widgets Migration
This guide helps you migrate from fully custom UI implementations to pre-built Fanfare widgets. Widgets provide a faster path to integration while still supporting customization.
| Benefit | Description |
|---|
| Less Code | No need to build state management or UI |
| Automatic Updates | Bug fixes and improvements without code changes |
| Consistent UX | Battle-tested user experience patterns |
| Accessibility | Built-in accessibility features |
| i18n Support | Multi-language support out of the box |
| Theming | Customizable via CSS variables |
Migration Approach
Replace your entire custom UI with a widget:
// Before: Custom implementation
function CustomQueue() {
const { status, position, enter, leave, isLoading, error } = useQueue("queue_123");
// 100+ lines of custom UI code...
return (
<div className="queue-container">
<header className="queue-header">...</header>
<div className="queue-body">...</div>
<footer className="queue-footer">...</footer>
</div>
);
}
// After: Widget
function WidgetQueue() {
return <fanfare-queue-widget queue-id="queue_123" />;
}
Option 2: Partial Customization with Slots
Keep some custom elements while using the widget structure:
import { QueueWidget } from "@waitify-io/fanfare-sdk-solid";
function PartiallyCustomQueue() {
return (
<QueueWidget
queueId="queue_123"
slots={{
header: (props) => (
<div className="my-brand-header">
<img src="/logo.svg" alt="Brand" />
<h2>{props.title}</h2>
</div>
),
// Use default for other slots
}}
/>
);
}
Option 3: Render Props for Full Control
Use render props to maintain full control while leveraging widget state:
<QueueWidget queueId="queue_123">
{({ status, position, enter, leave, isEntering, isLeaving }) => (
<YourCompletelyCustomUI status={status} position={position} onEnter={enter} onLeave={leave} />
)}
</QueueWidget>
Queue Migration
Custom Queue UI
function CustomQueueUI({ queueId }: { queueId: string }) {
const { isAuthenticated, guest } = useFanfareAuth();
const { queue, status, position, estimatedWait, admittanceToken, enter, leave, isLoading, error } = useQueue(queueId);
if (isLoading) {
return (
<div className="queue-loading">
<Spinner />
<p>Loading queue...</p>
</div>
);
}
if (error) {
return (
<div className="queue-error">
<h3>Error</h3>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
if (status === "admitted") {
return (
<div className="queue-admitted">
<CheckIcon />
<h2>You are In!</h2>
<a href={`/checkout?token=${admittanceToken}`}>Continue to Checkout</a>
</div>
);
}
return (
<div className="queue-ui">
<header className="queue-header">
<QueueIcon />
<h2>Virtual Waiting Room</h2>
<p>Join the queue for exclusive access</p>
</header>
{status === "queued" && (
<div className="queue-position">
<span className="position-number">{position}</span>
<span className="position-label">in line</span>
{estimatedWait && <p>Estimated wait: ~{estimatedWait} min</p>}
</div>
)}
<footer className="queue-actions">
{status === "queued" ? (
<button onClick={leave}>Leave Queue</button>
) : (
<button
onClick={async () => {
if (!isAuthenticated) await guest();
await enter();
}}
>
Enter Queue
</button>
)}
</footer>
</div>
);
}
function WidgetQueueUI({ queueId }: { queueId: string }) {
const widgetRef = useRef<HTMLElement>(null);
useEffect(() => {
const widget = widgetRef.current;
if (!widget) return;
const handleAdmitted = (e: CustomEvent<{ token: string }>) => {
window.location.href = `/checkout?token=${e.detail.token}`;
};
widget.addEventListener("fanfare-queue-admitted", handleAdmitted);
return () => widget.removeEventListener("fanfare-queue-admitted", handleAdmitted);
}, []);
return (
<fanfare-queue-widget
ref={widgetRef}
queue-id={queueId}
show-header="true"
show-estimated-wait="true"
show-actions="true"
/>
);
}
Draw Migration
Custom Draw UI
function CustomDrawUI({ drawId }: { drawId: string }) {
const { draw, status, result, timeUntilDraw, enter, withdraw, isLoading } = useDraw(drawId);
// ... 80+ lines of custom UI
}
function WidgetDrawUI({ drawId }: { drawId: string }) {
const widgetRef = useRef<HTMLElement>(null);
useEffect(() => {
const widget = widgetRef.current;
if (!widget) return;
const handleResult = (e: CustomEvent<{ won: boolean }>) => {
if (e.detail.won) {
showConfetti();
}
};
widget.addEventListener("fanfare-draw-result", handleResult);
return () => widget.removeEventListener("fanfare-draw-result", handleResult);
}, []);
return <fanfare-draw-widget ref={widgetRef} draw-id={drawId} />;
}
Auction Migration
Custom Auction UI
function CustomAuctionUI({ auctionId }: { auctionId: string }) {
const { auction, status, currentBid, myBid, minNextBid, bidHistory, timeRemaining, isWinning, placeBid, isLoading } =
useAuction(auctionId);
const [bidAmount, setBidAmount] = useState(minNextBid);
// ... 150+ lines of custom UI with bid form, history, countdown
}
function WidgetAuctionUI({ auctionId }: { auctionId: string }) {
const widgetRef = useRef<HTMLElement>(null);
useEffect(() => {
const widget = widgetRef.current;
if (!widget) return;
const handleOutbid = () => {
new Audio("/sounds/outbid.mp3").play();
};
const handleWin = (e: CustomEvent<{ token: string }>) => {
showConfetti();
window.location.href = `/checkout?token=${e.detail.token}`;
};
widget.addEventListener("fanfare-auction-outbid", handleOutbid);
widget.addEventListener("fanfare-auction-win", handleWin);
return () => {
widget.removeEventListener("fanfare-auction-outbid", handleOutbid);
widget.removeEventListener("fanfare-auction-win", handleWin);
};
}, []);
return <fanfare-auction-widget ref={widgetRef} auction-id={auctionId} show-bid-history="true" currency-code="USD" />;
}
Experience Journey Migration
Custom Journey Flow
function CustomExperienceFlow({ experienceId }: { experienceId: string }) {
const { journey, state, status, start } = useExperienceJourney(experienceId);
// Complex switch statement handling all journey stages
// 200+ lines of conditional rendering
}
function WidgetExperienceFlow({ experienceId }: { experienceId: string }) {
const widgetRef = useRef<HTMLElement>(null);
useEffect(() => {
const widget = widgetRef.current;
if (!widget) return;
const handleAdmitted = (e: CustomEvent<{ token: string }>) => {
window.location.href = `/checkout?token=${e.detail.token}`;
};
widget.addEventListener("fanfare-admitted", handleAdmitted);
return () => widget.removeEventListener("fanfare-admitted", handleAdmitted);
}, []);
return (
<fanfare-experience-widget
ref={widgetRef}
experience-id={experienceId}
auto-start="true"
checkout-url="/checkout"
/>
);
}
Styling Comparison
Custom CSS (Before)
.queue-container {
max-width: 400px;
margin: 0 auto;
padding: 24px;
border-radius: 12px;
background: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.queue-header h2 {
font-size: 1.5rem;
color: #1f2937;
}
.queue-button {
background: #3b82f6;
color: white;
padding: 12px 24px;
border-radius: 8px;
}
/* ... 50+ more rules */
fanfare-queue-widget {
--fanfare-primary: #3b82f6;
--fanfare-background: #ffffff;
--fanfare-foreground: #1f2937;
--fanfare-radius: 0.75rem;
--fanfare-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 400px;
margin: 0 auto;
}
What to Keep Custom
Not everything should be a widget. Keep custom implementations for:
- Unique brand experiences that differ significantly from standard patterns
- Complex integrations with other systems (shopping cart, user profiles)
- Custom animations beyond what CSS variables support
- A/B testing different UI approaches
Migration Checklist