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.

IDComponentSurface 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 } from '@revturbine/sdk';
import type { PlacementSlotProps } from '@revturbine/sdk';
// 1. Define the component
function FeedbackWidget({ content, onDismiss, onCtaClick }: PlacementSlotProps) {
return (
<div className="feedback-widget">
<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',
surfaceType: 'inline_embed',
component: FeedbackWidget,
priority: 10,
accepts: (output) => output.template_id === 'feedback_v1',
defaultProps: { dismissible: true },
});
FieldTypeRequiredDescription
idstringUnique identifier (prefix with custom:)
labelstringHuman-readable label (shown in Studio)
descriptionstringWhat this slot type does
surfaceTypestringBase surface 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 {
// Decision data
output: PlacementOutput;
content: PlacementOutput['content'];
decision: RevTurbinePlacementDecision;
// Interaction callbacks
onDismiss: () => void;
onCtaClick: (target?: string) => void;
onCtaComplete: (target?: string) => void;
onSnooze: (seconds?: number) => void;
onImpression: () => void;
// Configuration
dismissible: boolean;
theme: RevTurbineTheme;
}

Pass your registry to SurfaceSlotComponent:

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

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

Custom slots should use the theme from props for consistent styling:

function CustomCard({ content, theme, onCtaClick }: PlacementSlotProps) {
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>
);
}