Our SDK is @builder.io/react": "^8.0.2",
Our hero component is like the following under component > heroNew folder:
import type { StyledHeroNewProps } from '@/components/HeroNew/consts';
import Heading from '@/molecules/Heading';
import { StyledHeadingWithEyebrow } from '@/molecules/HeadingWithEyebrow/styled';
import Text from '@/molecules/Text';
import { mediaQuery } from '@/styles/breakpoints/ts/breakpoints';
import { colorToken } from '@/styles/colors/ts/colorToken';
import { effects } from '@/styles/effects/ts/effects';
import { borderRadii } from '@/styles/radii/ts/borderRadii';
import { spacings } from '@/styles/spacings/ts/spacings';
import type { StyledType } from '@/utils/types/styledTypeConverter';
import styled, { css } from 'styled-components';
import { headingLetterSpacings, headingLineHeights, headingSizes } from '@/styles/typography/ts/typography';
import HeadingWithEyebrow from '@/molecules/HeadingWithEyebrow';
import { DSNextImage } from '@/components/DSNextImage';
export const StyledHeroNew = styled.section<StyledType<StyledHeroNewProps & { hasOverflowingMedia: boolean }>>`
display: flex;
flex-direction: column;
gap: ${spacings.spacing5XL};
align-items: center;
justify-content: center;
padding: calc(${spacings.spacing5XL} + 2rem) ${spacings.spacingXL}
calc(${({ $hasOverflowingMedia }) => ($hasOverflowingMedia ? '3' : '1')} * ${spacings.spacing5XL}); // 2rem because of crappy layout
margin: 0 10px;
${({ $marginNav }) => $marginNav && `margin-top: ${spacings.spacing4XL};`}
border-radius: ${borderRadii.radiusLG};
position: relative;
${({ $backgroundColor }) => {
if (!$backgroundColor) return;
if ($backgroundColor in effects) {
return `background: ${effects[$backgroundColor as keyof typeof effects]}`;
}
return `background-color: ${colorToken[$backgroundColor as keyof typeof colorToken]}`;
}};
${mediaQuery.small} {
margin: 0 ${spacings.spacingXL};
${({ $marginNav }) => $marginNav && `margin-top: ${spacings.spacing7XL};`}
gap: ${spacings.spacing6XL};
padding: calc(${spacings.spacing6XL} + 2.25rem) ${spacings.spacingXL}
calc(${({ $hasOverflowingMedia }) => ($hasOverflowingMedia ? '3' : '1')} * ${spacings.spacing6XL}); // 2.25rem because of crappy layout (nav overlay)
}
${mediaQuery.medium} {
gap: ${({ $variant }) => ($variant === '2column' ? spacings.spacing8XL : spacings.spacing7XL)};
padding: calc(${spacings.spacing7XL} + 2.25rem) ${spacings.spacingXL}
calc(${({ $hasOverflowingMedia }) => ($hasOverflowingMedia ? '3' : '1')} * ${spacings.spacing7XL}); // 2.25rem because of crappy layout (nav overlay)
${({ $variant }) => $variant === '2column' && 'flex-direction: row;'}
}
`;
export const StyledDSBackgroundImage = styled(DSNextImage)`
position: absolute;
z-index: 1;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 100%;
object-fit: contain;
display: none;
${mediaQuery.medium} {
display: block;
}
`;
export const StyledDSBottomImage = styled(DSNextImage)`
display: block;
margin: ${spacings.spacingLG} auto 0;
max-width: 100%;
object-fit: contain;
${mediaQuery.medium} {
display: none;
}
`;
export const ContentWrapper = styled.div<{ $variant?: '1column' | '2column' }>`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: ${spacings.spacing5XL};
${mediaQuery.small} {
gap: ${spacings.spacing6XL};
}
${mediaQuery.medium} {
gap: ${spacings.spacing7XL};
${({ $variant }) =>
$variant === '2column'
? css`
max-width: 30rem; // 480px
text-align: left;
align-items: flex-start;
`
: css`
max-width: 48.75rem; // 780px
`};
}
`;
export const ContentInner = styled.div<{ $variant?: '1column' | '2column' }>`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
z-index: 2;
${mediaQuery.small} {
max-width: 36.25rem; // 580px
}
${mediaQuery.medium} {
max-width: unset;
${({ $variant }) =>
$variant === '2column' &&
css`
text-align: left;
align-items: flex-start;
// force the heading & eyebrow to the left in 2column desktop
${StyledHeadingWithEyebrow} {
align-items: flex-start;
text-align: left;
}
h1 {
text-align: left;
}
div > div > div > button {
width: 100%;
}
`};
}
`;
export const StyledHeading = styled(HeadingWithEyebrow)`
> *:last-child {
font-size: ${headingSizes.heading1};
line-height: ${headingLineHeights.heading1};
letter-spacing: ${headingLetterSpacings.heading1};
${mediaQuery.medium} {
${({ type }) =>
type === 'heading2' &&
css`
font-size: ${headingSizes.heading2};
line-height: ${headingLineHeights.heading2};
letter-spacing: ${headingLetterSpacings.heading2};
`};
}
}
`;
export const Subtitle = styled(Heading)`
font-size: ${headingSizes.heading3};
line-height: ${headingLineHeights.heading3};
letter-spacing: ${headingLetterSpacings.heading3};
margin-top: ${spacings.spacingXS};
${mediaQuery.small} {
margin-top: ${spacings.spacingSM};
}
${mediaQuery.medium} {
${({ type }) =>
type === 'heading4' &&
css`
font-size: ${headingSizes.heading4};
line-height: ${headingLineHeights.heading4};
letter-spacing: ${headingLetterSpacings.heading4};
`};
}
`;
export const Message = styled(Text)`
margin-top: ${spacings.spacingLG};
${mediaQuery.small} {
margin-top: ${spacings.spacing2XL};
}
`;
export const ButtonWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
gap: ${spacings.spacingMD};
align-items: stretch;
margin-top: ${spacings.spacing2XL};
${mediaQuery.small} {
width: auto;
flex-direction: row;
justify-content: center;
gap: ${spacings.spacingLG};
margin-top: ${spacings.spacing3XL};
}
`;
export const SecondColumn = styled.div<{ $variant?: '1column' | '2column' }>`
width: 100%;
${({ $variant }) =>
$variant === '1column' &&
css`
display: flex;
justify-content: center;
`};
${mediaQuery.medium} {
${({ $variant }) =>
$variant === '2column' &&
css`
width: calc(50% - ${spacings.spacingSM});
max-width: 36.25rem;
flex-shrink: 0;
`};
}
`;
export const MediaWrapper = styled.div`
width: 100%;
max-width: 73.75rem; // 1180px
border-radius: ${borderRadii.radiusSM};
position: relative;
`;
export const FormWrapper = styled.div`
max-width: 73.75rem; // 1180px
`;
export const OverflowingMedia = styled.div`
margin: calc(-2 * ${spacings.spacing5XL}) auto ${spacings.spacing5XL};
width: 100%;
max-width: calc(100% - (2 * ${spacings.spacingXL}));
${mediaQuery.small} {
margin: calc(-2 * ${spacings.spacing6XL}) auto ${spacings.spacing6XL};
max-width: calc(100% - (4 * ${spacings.spacingXL}));
}
${mediaQuery.medium} {
margin: calc(-2 * ${spacings.spacing7XL}) auto ${spacings.spacing7XL};
max-width: min(calc(100% - (4 * ${spacings.spacingXL})), 73.75rem);
}
`;
and our builder config file is separated into two in hero-presenter folder:
index.tsx
import type { BackgroundImage, HeroNewProps } from '@/components/HeroNew/consts';
import HeroNew from '@/components/HeroNew';
import type { HeadingWithEyebrowProps } from '@/molecules/HeadingWithEyebrow/consts';
import { type BasePresenterProps, builderPresenter } from '@/utils/builder/builderPresenter';
import injectSizesIntoImages from '@/utils/builder/injectSizesIntoImages';
import { Builder, BuilderBlockComponent, BuilderBlocks } from '@builder.io/react';
import type { BuilderElement } from '@builder.io/sdk';
import styled from 'styled-components';
// Properties for the presenter (must match Builder inputs)
export type HeroPresenterProps = BasePresenterProps & {
variant: '1column' | '2column';
eyebrow?: HeadingWithEyebrowProps['eyebrow'];
title: string;
subtitle?: string;
message?: string;
media?: BuilderElement[];
enableForm?: boolean;
enableImage?: boolean;
form?: BuilderElement[];
backgroundColor?: 'bgSecondary' | '';
darkMode?: false;
backgroundImage?: BackgroundImage;
marginNav?: boolean;
};
const StyledBuilderBlocks = styled(BuilderBlocks)`
display: flex;
flex-direction: column;
align-items: center !important;
.builder-block {
display: flex;
width: 100%;
justify-content: center;
}
`;
// Props transformation
export const transformHeadingProps = ({
variant,
eyebrow,
title,
subtitle,
message,
media,
enableForm,
form,
enableImage,
backgroundColor,
darkMode,
backgroundImage,
builderBlock,
marginNav,
}: HeroPresenterProps): HeroNewProps => ({
variant,
title: {
eyebrow,
children: title,
},
subtitle,
message,
buttons: builderBlock.children?.map(child => <BuilderBlockComponent block={child} key={child.id} />),
media:
enableImage && (Builder.isEditing || (media && media?.length > 0)) ? (
<StyledBuilderBlocks
blocks={injectSizesIntoImages(media, variant === '2column' ? '(min-width: 1024px) 50vw, 100vw' : '100vw')}
parentElementId={builderBlock.id}
dataPath="component.options.media"
child
/>
) : undefined,
form: enableForm ? (
<BuilderBlocks blocks={form} parentElementId={builderBlock.id} dataPath="component.options.form" child />
) : undefined,
backgroundColor: backgroundColor || undefined,
darkMode: darkMode,
backgroundImage: backgroundImage || undefined,
marginNav,
});
// Export the presenter
export default builderPresenter(HeroNew, transformHeadingProps);
and hero-builder.tsx
import { EyebrowInputs } from '@/builder-presenters/Heading/heading.builder';
import type { Component, Input } from '@builder.io/sdk';
import { getBuilderBackgroundColors } from '@/utils/builder/builderConfigHelpers';
export const HeroInputs: Input[] = [
{
name: 'variant',
type: 'text',
enum: ['1column', '2column'],
defaultValue: '1column',
},
{
name: 'eyebrow',
type: 'object',
subFields: EyebrowInputs,
},
{
name: 'title',
type: 'string',
required: true,
defaultValue: 'Hero Title',
},
{
name: 'subtitle',
type: 'string',
defaultValue: 'Hero Subtitle',
},
{
name: 'message',
type: 'string',
defaultValue: 'Hero Message',
},
{
name: 'enableImage',
friendlyName: 'Enable Media',
helperText: 'Enable overflown image/video/embed in the hero section',
type: 'boolean',
defaultValue: false,
},
{
name: 'media',
type: 'uiBlocks',
hideFromUI: true,
defaultValue: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'dsImage',
options: {
image: 'https://picsum.photos/1200/800',
width: 1200,
height: 800,
},
},
},
],
},
{
name: 'enableForm',
friendlyName: 'Enable Form',
helperText: 'Enable form in the hero section.',
type: 'boolean',
},
{
name: 'form',
type: 'uiBlocks',
hideFromUI: true,
defaultValue: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'Form',
options: {
formId: '1071',
showHeading: true,
businessEmailValidation: false,
redirectUrl: '',
paddingDisabled: false,
submittedBox: {
hasCta: false,
heading: 'Heading',
text: 'Text',
},
failedBox: {
hasCta: false,
heading: 'Heading',
text: 'Text',
},
},
},
},
],
},
{
name: 'backgroundColor',
type: 'string',
enum: [
...getBuilderBackgroundColors(),
{
label: 'Blue Gradient',
value: 'gradientBlue',
},
],
defaultValue: 'bgSecondary',
},
{
name: 'darkMode',
type: 'boolean',
friendlyName: 'Enable Dark Mode',
defaultValue: false,
},
{
name: 'backgroundImage',
friendlyName: 'Background Image',
type: 'object',
subFields: [
{
name: 'src',
type: 'file',
allowedFileTypes: ['jpeg', 'png', 'jpg', 'svg', 'gif'],
},
{
name: 'alt',
type: 'string',
},
],
},
{
name: 'marginNav',
type: 'boolean',
friendlyName: 'Margin for navigation (NOT RECOMMENDED)',
defaultValue: false,
},
];
export const HeroComponent: Component = {
name: 'Hero',
image:
'https://cdn.builder.io/api/v1/image/assets%2F1d8ecee591ac4358befb8fe998100548%2F2ae2a73e186f41f090aa0ed18448c6b6',
canHaveChildren: true,
noWrap: true,
inputs: HeroInputs,
childRequirements: {
message: 'You can only add Button, Video Button and Marketo Button here',
query: {
'component.name': { $in: ['dsButton', 'FormButton', 'videoButton', 'Image', 'dsImage'] },
},
},
defaultChildren: [
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'dsButton',
options: {
children: 'Primary button',
type: 'primary',
},
},
},
{
'@type': '@builder.io/sdk:Element',
component: {
name: 'dsButton',
options: {
children: 'Secondary button',
type: 'secondary',
},
},
},
],
};