Headless Mode
For complete design freedom, use Fanfare React hooks to build your own UI components from scratch. This approach gives you full control over markup, styling, and behavior.When to Use Headless
| Use Case | Recommended Approach |
|---|---|
| Quick integration | Web Components |
| Minor styling changes | CSS Variables |
| Partial UI customization | Slots/Render Props |
| Complete design freedom | Headless Hooks |
| Brand-specific design system | Headless Hooks |
| Complex animations | Headless Hooks |
Available Hooks
| Hook | Purpose |
|---|---|
useQueue | Virtual waiting room |
useDraw | Lottery/raffle |
useAuction | Real-time bidding |
useWaitlist | Pre-registration signup |
useTimedRelease | Flash sale / time-window access |
useExperienceJourney | Full journey orchestration |
useFanfareAuth | Authentication management |
Complete Headless Queue Example
Copy
import { useQueue, useFanfareAuth } from "@waitify-io/fanfare-sdk-react";
import { useState } from "react";
function HeadlessQueue({ queueId }: { queueId: string }) {
const { isAuthenticated, guest } = useFanfareAuth();
const { queue, status, position, estimatedWait, admittanceToken, enter, leave, isLoading, error } = useQueue(queueId);
const handleEnter = async () => {
if (!isAuthenticated) {
await guest();
}
await enter();
};
// Loading state
if (isLoading) {
return (
<div className="queue-loading">
<div className="spinner" />
<p>Loading queue...</p>
</div>
);
}
// Error state
if (error) {
return (
<div className="queue-error">
<h3>Something went wrong</h3>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Try Again</button>
</div>
);
}
// Admitted state
if (status === "admitted" && admittanceToken) {
return (
<div className="queue-admitted">
<div className="success-icon">✓</div>
<h2>You are In!</h2>
<p>You have been admitted to the experience.</p>
<a href={`/checkout?token=${admittanceToken}`} className="checkout-button">
Continue to Checkout
</a>
</div>
);
}
// Queued state
if (status === "queued") {
return (
<div className="queue-waiting">
<div className="position-display">
<span className="position-number">{position}</span>
<span className="position-label">in line</span>
</div>
{estimatedWait && <p className="wait-estimate">Estimated wait: ~{estimatedWait} minutes</p>}
<div className="queue-info">
<p>Please keep this page open.</p>
<p>We will notify you when it is your turn.</p>
</div>
<button onClick={leave} className="leave-button">
Leave Queue
</button>
</div>
);
}
// Default: Entry state
return (
<div className="queue-entry">
<h2>Join the Queue</h2>
<p>Enter our virtual waiting room to access this exclusive experience.</p>
<button onClick={handleEnter} className="enter-button">
Enter Queue
</button>
<div className="queue-benefits">
<h4>While you wait:</h4>
<ul>
<li>Your spot is saved</li>
<li>Real-time position updates</li>
<li>Automatic admission when ready</li>
</ul>
</div>
</div>
);
}
Complete Headless Experience Journey
Copy
import { useExperienceJourney, useFanfareAuth, useTranslations } from "@waitify-io/fanfare-sdk-react";
import { useState } from "react";
function HeadlessExperience({ experienceId }: { experienceId: string }) {
const { isAuthenticated, isGuest, guest } = useFanfareAuth();
const t = useTranslations();
const { journey, state, status, error, start } = useExperienceJourney(experienceId);
const [accessCode, setAccessCode] = useState("");
const handleStart = async (code?: string) => {
if (!isAuthenticated && !isGuest) {
await guest();
}
await start({ accessCode: code });
};
// Render based on status
switch (status) {
case "idle":
return <StartScreen onStart={() => handleStart()} />;
case "entering_experience":
case "routing_sequence":
case "loading_distributions":
return <LoadingScreen message="Setting up your experience..." />;
case "needs_authentication":
return <AuthScreen onComplete={() => journey?.authenticate()} />;
case "needs_access_code":
return (
<AccessCodeScreen
value={accessCode}
onChange={setAccessCode}
onSubmit={() => handleStart(accessCode)}
onSkip={() => journey?.skipAccessCode()}
/>
);
case "waiting":
return <WaitingScreen snapshot={state?.snapshot} onJoinWaitlist={() => journey?.enterWaitlist()} />;
case "ready":
return <ReadyScreen journey={journey!} state={state!} />;
case "no_sequence_available":
return <NoAccessScreen />;
case "error":
return <ErrorScreen error={error} onRetry={() => start()} />;
default:
return <LoadingScreen />;
}
}
// Component implementations
function StartScreen({ onStart }: { onStart: () => void }) {
return (
<div className="start-screen">
<h1>Welcome</h1>
<p>Ready to begin your experience?</p>
<button onClick={onStart} className="start-button">
Get Started
</button>
</div>
);
}
function LoadingScreen({ message = "Loading..." }: { message?: string }) {
return (
<div className="loading-screen">
<div className="loader" />
<p>{message}</p>
</div>
);
}
function AuthScreen({ onComplete }: { onComplete: () => void }) {
const [email, setEmail] = useState("");
return (
<div className="auth-screen">
<h2>Sign In</h2>
<form
onSubmit={(e) => {
e.preventDefault();
onComplete();
}}
>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Enter your email" />
<button type="submit">Continue</button>
</form>
</div>
);
}
function AccessCodeScreen({
value,
onChange,
onSubmit,
onSkip,
}: {
value: string;
onChange: (v: string) => void;
onSubmit: () => void;
onSkip?: () => void;
}) {
return (
<div className="access-code-screen">
<h2>VIP Access</h2>
<p>Enter your access code for priority entry</p>
<input type="text" value={value} onChange={(e) => onChange(e.target.value)} placeholder="Access code" />
<button onClick={onSubmit}>Submit</button>
{onSkip && (
<button onClick={onSkip} className="secondary">
Skip
</button>
)}
</div>
);
}
function WaitingScreen({ snapshot, onJoinWaitlist }: { snapshot: JourneySnapshot | null; onJoinWaitlist: () => void }) {
const isOnWaitlist = snapshot?.sequenceStage === "waitlist_entered";
return (
<div className="waiting-screen">
{isOnWaitlist ? (
<>
<h2>You are on the Waitlist</h2>
<p>We will notify you when the experience opens.</p>
</>
) : (
<>
<h2>Coming Soon</h2>
<p>Join the waitlist to be notified when we open.</p>
<button onClick={onJoinWaitlist}>Join Waitlist</button>
</>
)}
</div>
);
}
function ReadyScreen({ journey, state }: { journey: ExperienceJourney; state: ExperienceJourneyState }) {
const { snapshot } = state;
const distributionType = snapshot.context.distribution?.active?.type;
const handleEnter = () => {
const actionMap: Record<string, string> = {
queue: "enter_queue",
draw: "enter_draw",
auction: "enter_auction",
timed_release: "enter_timed_release",
};
journey.perform(actionMap[distributionType || "queue"]);
};
if (snapshot.sequenceStage === "admitted") {
return (
<div className="admitted-screen">
<h2>You are In!</h2>
<a href={`/checkout?token=${snapshot.context.admittanceToken}`}>Continue to Checkout</a>
</div>
);
}
if (snapshot.sequenceStage === "participating") {
return (
<div className="participating-screen">
<h2>You are participating</h2>
<p>Please wait for your turn...</p>
<button onClick={() => journey.perform("leave_participation")}>Leave</button>
</div>
);
}
return (
<div className="ready-screen">
<h2>Ready to Enter</h2>
<p>The experience is now open!</p>
<button onClick={handleEnter}>Enter Now</button>
</div>
);
}
function ErrorScreen({ error, onRetry }: { error: string | null; onRetry: () => void }) {
return (
<div className="error-screen">
<h2>Something went wrong</h2>
<p>{error || "An unexpected error occurred"}</p>
<button onClick={onRetry}>Try Again</button>
</div>
);
}
function NoAccessScreen() {
return (
<div className="no-access-screen">
<h2>No Access Available</h2>
<p>This experience is not currently available to you.</p>
</div>
);
}
Styling Headless Components
Since you control all markup, use any styling approach:CSS Modules
Copy
import styles from "./Queue.module.css";
function HeadlessQueue() {
const { status, position } = useQueue("queue_123");
return (
<div className={styles.queue}>
<span className={styles.position}>{position}</span>
</div>
);
}
Tailwind CSS
Copy
function HeadlessQueue() {
const { status, position, enter } = useQueue("queue_123");
return (
<div className="flex flex-col items-center gap-4 rounded-lg bg-white p-6 shadow-lg">
<span className="text-4xl font-bold text-blue-600">{position}</span>
<button onClick={enter} className="rounded-lg bg-blue-600 px-6 py-3 text-white transition hover:bg-blue-700">
Enter Queue
</button>
</div>
);
}
Styled Components
Copy
import styled from "styled-components";
const QueueContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
`;
const Position = styled.span`
font-size: 3rem;
font-weight: bold;
color: ${(props) => props.theme.primary};
`;
function HeadlessQueue() {
const { position } = useQueue("queue_123");
return (
<QueueContainer>
<Position>{position}</Position>
</QueueContainer>
);
}
Best Practices
- Handle all states - Loading, error, and all distribution states
- Provide feedback - Show loading indicators for async actions
- Accessibility - Ensure proper ARIA labels and keyboard navigation
- Responsive design - Test across screen sizes
- Error boundaries - Wrap in React error boundaries
Related
- React Hooks Overview - All available hooks
- useExperienceJourney - Journey orchestration
- Component Customization - Slots and render props