Skip to content

Custom Slot Types

import { Aside } from ‘@astrojs/starlight/components’;

The SDK ships with 11 built-in slot components (banner, modal, toast, etc.). When those don’t fit your use case, you can register custom slot types.

IDComponentComponent Type
bannerBannerSlotbanner
modalModalSlotmodal
toastToastSlottoast
inline_embedInlineEmbedSlotinline_embed
buttonButtonSlotbutton
quota_meterQuotaMeterSlotquota_meter
full_pageFullPageSlotfull_page
cliCliSlotcli
credit_balanceCreditBalanceSlotcredit_balance
tooltipTooltipSlottooltip
agent_connectorAgentConnectorSlotagent_connector

Create a PlacementSlotType definition and register it:

import { PlacementTypeRegistry, useRevTurbineTheme } from '@revturbine/sdk';
import type { PlacementSlotProps } from '@revturbine/sdk';
// 1. Define the component
function FeedbackWidget({ content, onDismiss, onCtaClick, exposureRef }: PlacementSlotProps) {
const theme = useRevTurbineTheme();
return (
<div ref={exposureRef} className="feedback-widget" style={{ color: theme.colors.text }}>
<p>{content?.body}</p>
<div>
<button onClick={onCtaClick}>{content?.cta_label ?? 'Submit'}</button>
<button onClick={onDismiss}>Not now</button>
</div>
</div>
);
}
// 2. Register it
const registry = new PlacementTypeRegistry();
registry.register({
id: 'custom:feedback-widget',
label: 'Feedback Widget',
description: 'In-app feedback collection prompt',
// One of the SDK's component types — see the table below.
componentType: 'in_page',
component: FeedbackWidget,
priority: 10,
// The template id lives on the decision's surface, not at the top level.
accepts: (output) => output.surface.template === 'feedback_v1',
});
FieldTypeRequiredDescription
idstringUnique identifier (prefix with custom:)
labelstringHuman-readable label (shown in Studio)
descriptionstringWhat this slot type does
componentTypeRevTurbineComponentTypeCanonical component type
componentComponentType<PlacementSlotProps>React component to render
accepts(output) => booleanPredicate to match specific placements
prioritynumberHigher = evaluated first (default: 0)
defaultPropsPartial<PlacementSlotProps>Default props merged into component

All slot components (built-in and custom) receive the same props:

interface PlacementSlotProps {
placement: PlacementOutput;
content: ResolvedContent;
uiPath: PlacementUiPath;
promotion?: PlacementPromotion;
// Renderer callbacks
onCtaClick: () => void;
onSecondaryCtaClick?: () => void;
onDismiss: () => void;
onRemindLater?: () => void;
visible: boolean;
className?: string;
style?: React.CSSProperties;
exposureRef?: (element: Element | null) => void;
}

Pass your registry to <Slot>:

<Slot
id="feedback_slot"
surfaceTemplateIds={['feedback_v1']}
registry={registry}
/>

The SDK evaluates registered types by priority, calling accepts() on each until one matches.

Custom slots obtain the active theme with useRevTurbineTheme():

function CustomCard({ content, onCtaClick }: PlacementSlotProps) {
const theme = useRevTurbineTheme();
return (
<div style={{
background: theme.colors.surface,
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.colors.surfaceBorder}`,
fontFamily: theme.typography.fontFamily,
color: theme.colors.text,
padding: 16,
}}>
<h3 style={{ fontSize: theme.typography.fontSizeHeader }}>
{content?.header}
</h3>
<p>{content?.body}</p>
<button
style={{
background: theme.colors.primary,
color: theme.colors.primaryText,
borderRadius: theme.shape.borderRadiusSmall,
}}
onClick={() => onCtaClick()}
>
{content?.cta_label}
</button>
</div>
);
}