Skip to main content

Component Customization

Fanfare widgets support customization through slots and render props, allowing you to replace specific parts while keeping the widget functionality.

Customization Approaches

ApproachCustomization LevelComplexity
CSS VariablesStyling onlyLow
SlotsPartial replacementMedium
Render PropsFull controlHigh
HooksComplete freedomHighest

Slots (SolidJS/Web Components)

Widgets built with SolidJS support slot-based customization:
// Using Solid SDK directly
import { QueueWidget } from "@waitify-io/fanfare-sdk-solid";

function CustomQueueWidget() {
  return (
    <QueueWidget
      queueId="queue_123"
      slots={{
        header: (props) => (
          <div className="custom-header">
            <img src="/logo.svg" alt="Brand" />
            <h2>{props.title}</h2>
            <p>Position: {props.position}</p>
          </div>
        ),
        position: (props) => (
          <div className="custom-position">
            <span className="number">{props.position}</span>
            <span className="label">in line</span>
          </div>
        ),
        actions: (props) => (
          <div className="custom-actions">
            {props.status === "enterable" && (
              <button onClick={props.onEnter} disabled={props.isEntering}>
                {props.isEntering ? "Joining..." : "Join Now"}
              </button>
            )}
            {props.status === "queued" && (
              <button onClick={props.onLeave} disabled={props.isLeaving}>
                Leave Queue
              </button>
            )}
          </div>
        ),
      }}
    />
  );
}

Available Slots by Widget

QueueWidget Slots

SlotProps
headerstatus, position, estimatedWaitMinutes, title, description
positionstatus, position, estimatedWaitMinutes
actionsstatus, onEnter, onLeave, isEntering, isLeaving

DrawWidget Slots

SlotProps
headerstatus, drawTime, timeRemaining, title, description
countdownstatus, drawTime, timeRemaining
actionsstatus, onEnter, onWithdraw, onProceed, isEntering

AuctionWidget Slots

SlotProps
headerstatus, currentBid, myBid, isWinning, title, description
bidDisplaystatus, currentBid, myBid, minNextBid, reservePrice, reserveMet
bidFormstatus, minNextBid, bidIncrement, onBid, isBidding
bidHistorystatus, bidHistory
countdownstatus, endTime, timeRemaining

ExperienceWidget Slots

SlotProps
startsnapshot, status, onStart, isStarting
loadingsnapshot, status, message
authsnapshot, status, onSubmit, onVerify, onSkip
accessCodesnapshot, status, onSubmit, onSkip
upcomingsnapshot, status, startsAt, canEnterWaitlist, onEnterWaitlist
waitlistsnapshot, status, position, startsAt, onLeaveWaitlist
enterablesnapshot, status, participationType, onEnter
participatingsnapshot, status, participationType, position, onLeave
admittedsnapshot, status, admittanceToken, expiresAt
expiredsnapshot, status, onReenter, isReentering
endedsnapshot, status, endedAt
errorsnapshot, status, error, onRetry

Render Props

For complete control, use the render prop pattern:
import { QueueWidget } from "@waitify-io/fanfare-sdk-solid";

function FullyCustomQueue() {
  return (
    <QueueWidget queueId="queue_123">
      {({ status, position, estimatedWaitMinutes, enter, leave, isEntering, isLeaving, isLoading, error }) => (
        <div className="my-queue-design">
          {isLoading ? (
            <MyLoadingSpinner />
          ) : error ? (
            <MyErrorDisplay error={error} />
          ) : status === "admitted" ? (
            <MySuccessView />
          ) : status === "queued" ? (
            <MyQueuedView
              position={position}
              estimatedWait={estimatedWaitMinutes}
              onLeave={leave}
              isLeaving={isLeaving}
            />
          ) : (
            <MyEnterView onEnter={enter} isEntering={isEntering} />
          )}
        </div>
      )}
    </QueueWidget>
  );
}

React Hook Alternative

For maximum flexibility with React, use hooks instead of widgets:
import { useQueue } from "@waitify-io/fanfare-sdk-react";

function CompletelyCustomQueue() {
  const { queue, status, position, estimatedWait, enter, leave, isLoading, error } = useQueue("queue_123");

  // Build your own UI with full design freedom
  return (
    <div className="my-brand-queue">
      <MyBrandHeader />

      {status === "queued" && <MyBrandQueuePosition position={position} estimatedWait={estimatedWait} />}

      <MyBrandActions status={status} onEnter={enter} onLeave={leave} isLoading={isLoading} />

      {error && <MyBrandError error={error} />}
    </div>
  );
}

Composing Custom Components

Custom Header Component

interface CustomHeaderProps {
  status: string;
  title: string;
  subtitle?: string;
  icon?: React.ReactNode;
}

function CustomHeader({ status, title, subtitle, icon }: CustomHeaderProps) {
  return (
    <div className="custom-header">
      <div className="header-icon">{icon || <DefaultIcon status={status} />}</div>
      <h2 className="header-title">{title}</h2>
      {subtitle && <p className="header-subtitle">{subtitle}</p>}
    </div>
  );
}

// Use in slot
<QueueWidget
  queueId="queue_123"
  slots={{
    header: (props) => (
      <CustomHeader
        status={props.status}
        title="Join Our VIP Line"
        subtitle={props.status === "queued" ? `You are #${props.position} in line` : "Get exclusive access"}
        icon={<VIPIcon />}
      />
    ),
  }}
/>;

Custom Action Buttons

interface CustomActionsProps {
  status: string;
  onEnter: () => Promise<void>;
  onLeave: () => Promise<void>;
  isEntering: boolean;
  isLeaving: boolean;
}

function CustomActions({ status, onEnter, onLeave, isEntering, isLeaving }: CustomActionsProps) {
  if (status === "admitted") {
    return (
      <a href="/checkout" className="checkout-button">
        Continue to Checkout
      </a>
    );
  }

  if (status === "queued") {
    return (
      <button onClick={onLeave} disabled={isLeaving} className="leave-button">
        {isLeaving ? "Leaving..." : "Leave Queue"}
      </button>
    );
  }

  return (
    <button onClick={onEnter} disabled={isEntering} className="enter-button">
      {isEntering ? (
        <>
          <Spinner /> Joining...
        </>
      ) : (
        "Join Queue"
      )}
    </button>
  );
}

Custom Position Display

interface CustomPositionProps {
  position: number;
  estimatedWaitMinutes: number | null;
}

function CustomPosition({ position, estimatedWaitMinutes }: CustomPositionProps) {
  return (
    <div className="position-display">
      <div className="position-number">
        <span className="number">{position}</span>
        <span className="label">in line</span>
      </div>

      {estimatedWaitMinutes && (
        <div className="wait-estimate">
          <ClockIcon />
          <span>
            {estimatedWaitMinutes < 60
              ? `~${estimatedWaitMinutes} min`
              : `~${Math.round(estimatedWaitMinutes / 60)} hr`}
          </span>
        </div>
      )}

      <ProgressBar value={100 - position} max={100} className="position-progress" />
    </div>
  );
}

Mixing Approaches

Combine approaches for the right balance:
// Use widget for structure, slots for specific customization
<ExperienceWidget
  experienceId="exp_123"
  autoStart
  slots={{
    // Custom admitted view
    admitted: (props) => <MyBrandSuccessView token={props.admittanceToken} expiresAt={props.expiresAt} />,
    // Custom error handling
    error: (props) => <MyBrandErrorView error={props.error} onRetry={props.onRetry} />,
    // Keep default for other stages
  }}
/>