Implementing Dynamic Zones
Dynamic zones are available through the React Components, React Hooks, and Web Components workflows — there is no Headless equivalent.
- React Components
- React Hooks
- Web Components
SurfsideDynamicZone renders whichever creative type wins a dynamic zone auction — banner, video, or carousel — without you needing to decide in advance.
import { SurfsideDynamicZone } from '@surfside/ads-react';
<SurfsideDynamicZone
zoneId="your-zone-id"
carousel={{
cardMinWidth: 200,
carouselLimits: { type: 'infinite' },
nextType: 'page'
}}
/>
Requires a SurfsideProvider2 ancestor.
What a dynamic zone is
A dynamic zone is a single zone ID configured in the Surfside platform to request multiple creative types in one bid request — for example, a 4×1 banner, an 8×1 banner, and a product card carousel — and fill whichever has demand. If no 4×1 banners are available, the bidder might fill with an 8×1 instead. If there are no banners at all, it might fall back to a carousel.
The creative types to try, their priority, and their fallback order are all configured in the Surfside platform against the zone ID. You need the correct dynamic zone ID from the platform to use this component — a standard zone ID will not work.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
zoneId | string | Yes | The dynamic zone ID from the Surfside platform |
carousel | ISurfsideCarouselRenderProps | No | Frontend rendering options applied when the zone fills with a carousel |
category | string[] | No | Category targeting hints passed to the bidder |
enabled | boolean | No | When false, the component renders nothing. Default true |
onError | (error: ErrResult) => void | No | Called when the zone fails to fill or an error occurs |
The carousel prop
When a dynamic zone fills with a carousel, some of the carousel's behavior is configured in the Surfside platform — for example, what kind of recommender to use. However, frontend layout details like card sizing and scroll behavior are not something the backend can know about, so they have to be passed from your application.
If the zone returns a carousel and you do not provide carousel, the component falls back to the defaults used by the library. Pass carousel to override them:
<SurfsideDynamicZone
zoneId="your-zone-id"
carousel={{
cardMinWidth: 200,
carouselLimits: { type: 'limited', limit: 6 },
nextType: 'product'
}}
/>
ISurfsideCarouselRenderProps
| Field | Type | Description |
|---|---|---|
cardMinWidth | number | Minimum pixel width of each product card |
carouselLimits | { type: 'infinite' } | { type: 'limited', limit: number } | Whether the carousel loads more products as the user scrolls, or shows a fixed number |
nextType | 'page' | 'product' | Controls how the next set of products is fetched — by page or by individual product |
Category targeting
Pass category to narrow which ads are eligible for the placement:
<SurfsideDynamicZone
zoneId="your-zone-id"
carousel={{ cardMinWidth: 200, carouselLimits: { type: 'infinite' }, nextType: 'page' }}
category={['edibles']}
/>
Error handling
Use onError to react when the zone fails to fill:
<SurfsideDynamicZone
zoneId="your-zone-id"
carousel={{ cardMinWidth: 200, carouselLimits: { type: 'infinite' }, nextType: 'page' }}
onError={(err) => console.warn('Dynamic zone did not fill', err.error)}
/>
useSurfsideDynamicZone runs the dynamic zone creative selection process and returns whichever creative type wins — banner, video, or products — along with that creative type's full hook response. Use it when you need direct control over rendering each creative type rather than the SurfsideDynamicZone component.
import { useSurfsideDynamicZone } from '@surfside/ads-react';
const { data, type, loading, error } = useSurfsideDynamicZone({
zoneId: 'your-zone-id',
pageSize: 6
});
if (loading) {
return <div>Loading...</div>;
}
if (error !== undefined) {
throw error.error;
}
switch (type) {
case 'banner': return <MyBannerRenderer banner={data} />;
case 'video': return <MyVideoRenderer video={data} />;
case 'products': return <MyCarouselRenderer products={data} />;
}
Requires a SurfsideProvider2 ancestor.
What a dynamic zone is
A dynamic zone is a single zone ID configured in the Surfside platform to request multiple creative types in one bid request and fill whichever has demand. The creative types, their priority, and fallback order are all configured in the platform against the zone ID. You need the correct dynamic zone ID from the platform — a standard zone ID will not work.
Options
| Option | Type | Required | Description |
|---|---|---|---|
zoneId | string | Yes | The dynamic zone ID from the Surfside platform |
pageSize | number | Yes | Number of products per page, used when the zone fills with a carousel |
enabled | boolean | No | When false, no request is made. Default true |
withProductInfo | boolean | No | When true, returns full product data if a carousel fills. When false, returns only product IDs. Default true |
category | string[] | No | Category targeting hints passed to the bidder |
Return values
| Field | Type | Description |
|---|---|---|
type | 'banner' \| 'video' \| 'products' \| undefined | The winning creative type. undefined while loading or on error |
data | Creative response or undefined | The full hook response for the winning creative type (see below) |
loading | boolean | true during the creative selection process |
error | ErrResult \| undefined | Set if no creative fills or an error occurs |
refetchDynamicZone | () => Promise<void> | Re-triggers the creative selection process |
Handling each creative type
The data field is the full return value of the underlying hook for whichever creative type won. Use the type discriminator to branch and pass it to a typed renderer:
Banner
When type === 'banner', data is an IUseSurfsideBannerReturnSuccess. Render data.banner.adm. As with the Banners hook, fire trackWin with useLayoutEffect so it runs immediately before the banner paints rather than after:
const MyBannerRenderer = ({ banner }: { banner: IUseSurfsideBannerReturnSuccess }) => {
useLayoutEffect(() => {
banner.analytics?.trackWin();
}, [banner.analytics]);
return <div dangerouslySetInnerHTML={{ __html: banner.banner.adm }} />;
};
Video
When type === 'video', data is the video hook response. Render data.video.videoUrl and fire the win and impression analytics immediately before the video renders, using useLayoutEffect for the same reason:
const MyVideoRenderer = ({ video }: { video: IUseSurfsideVideoReturnSuccess }) => {
useLayoutEffect(() => {
video.analytics?.win();
video.analytics?.impression();
}, [video.analytics]);
return (
<video autoPlay muted>
<source src={video.video.videoUrl} type={video.video.mime} />
</video>
);
};
Products
When type === 'products', data is the products hook response. Render the data.products array and handle pagination with data.fetchNextPage:
const MyCarouselRenderer = ({ products }: { products: IUseSurfsideProductsReturnGenericSuccess<IProductInfoData> }) => {
return (
<div>
{products.products.map((product) => (
<div key={product.data.id}>
<img src={product.data.image} alt={product.data.name} />
<h2>{product.data.name}</h2>
{product.type === 'sponsored' && <span>Sponsored</span>}
</div>
))}
{products.hasMore && (
<button onClick={products.fetchNextPage}>Load more</button>
)}
</div>
);
};
Re-fetching
refetchDynamicZone re-runs the creative selection process. Because the zone can return a different creative type on each run, calling it may switch from a banner to a carousel, which could be jarring. Prefer using the refetch callback on the underlying creative's data if you want to re-auction the same creative type.
<surf-dynamic-zone> renders whichever creative type wins a dynamic zone auction — banner, video, product card, or carousel — without you needing to decide in advance. Like the React component, it handles the request, rendering, and tracking internally.
<surf-dynamic-zone
account-id="your-account-id"
site-id="your-site-id"
channel-id="your-channel-id"
location-id="your-location-id"
zone-id="your-dynamic-zone-id"
card-min-width="260"
next-type="product"
></surf-dynamic-zone>
Requires the Surfside script tag — see Initialization.
What a dynamic zone is
Same as the React workflows: a dynamic zone is a single zone ID configured in the Surfside platform to request multiple creative types in one bid request and fill whichever has demand. The creative types, their priority, fallback order, and recommender are all configured in the platform against the zone ID — a standard zone ID will not work. Unlike <surf-carousel>, there's no strategy or recommend attribute here; when the zone fills with a carousel, that behavior comes entirely from the platform configuration.
Attributes
| Attribute | Required | Default | Description |
|---|---|---|---|
account-id, site-id, channel-id, location-id | Yes | — | Your Surfside account identifiers — see Initialization |
zone-id | Yes | — | The dynamic zone ID from the Surfside platform |
card-min-width | No | 300 | Minimum pixel width of each product card, applied when the zone fills with a carousel |
max-items | No | Platform default | Caps the number of products loaded, when the zone fills with a carousel |
next-type | No | Platform default | product or page — controls how the next set of carousel products is fetched, when the zone fills with a carousel |
category | No | — | Comma-separated category targeting hints passed to the bidder |
keywords | No | — | Comma-separated keyword targeting hints passed to the bidder |
Updating a placement
Attributes are reactive. Changing zone-id, card-min-width, or any other attribute on an already-connected <surf-dynamic-zone> from JavaScript automatically triggers a new request and re-renders the placement.
No fill
If the zone fails to fill, the component renders nothing. There's no error event to listen for.