Implementing a Custom OnBoarding Tour Component with Ant Design 5 in React
This article walks through building a reusable OnBoarding (tour) component for React applications using Ant Design 5, explaining the underlying mask technique, step management, animation handling, resize observation, and providing complete source code and usage examples.
When a new feature is added to an application, developers often use an onboarding or tour component to guide users; Ant Design 5 includes such a component.
The article demonstrates how to implement a similar component with a simpler mask using a single div whose width , height and four directional border-width values are animated via CSS transitions.
First, a Vite + React project is created ( npx create-vite ) and the default styles are removed.
npx create-viteA Mask.tsx component is introduced, which receives the target element and container, computes mask styles, and renders optional mask content.
import React, { CSSProperties, useEffect, useState } from 'react';
import { getMaskStyle } from './getMaskStyle';
interface MaskProps {
element: HTMLElement;
container?: HTMLElement;
renderMaskContent?: (wrapper: React.ReactNode) => React.ReactNode;
}
export const Mask: React.FC
= (props) => {
const { element, renderMaskContent, container } = props;
const [style, setStyle] = useState
({});
useEffect(() => {
if (!element) return;
element.scrollIntoView({ block: 'center', inline: 'center' });
const style = getMaskStyle(element, container || document.documentElement);
setStyle(style);
}, [element, container]);
const getContent = () => {
if (!renderMaskContent) return null;
return renderMaskContent(
);
};
return (
{getContent()}
);
};The helper getMaskStyle.ts calculates the mask dimensions and border widths based on the target element’s bounding rectangle and the container’s scroll offsets.
export const getMaskStyle = (element: HTMLElement, container: HTMLElement) => {
if (!element) return {};
const { height, width, left, top } = element.getBoundingClientRect();
const elementTopWithScroll = container.scrollTop + top;
const elementLeftWithScroll = container.scrollLeft + left;
return {
width: container.scrollWidth,
height: container.scrollHeight,
borderTopWidth: Math.max(elementTopWithScroll, 0),
borderLeftWidth: Math.max(elementLeftWithScroll, 0),
borderBottomWidth: Math.max(container.scrollHeight - height - elementTopWithScroll, 0),
borderRightWidth: Math.max(container.scrollWidth - width - elementLeftWithScroll, 0),
};
};The mask’s CSS (in index.scss ) positions it absolutely, gives it a semi‑transparent border, and adds a transition for smooth animation.
.mask {
position: absolute;
left: 0;
top: 0;
z-index: 999;
border-style: solid;
box-sizing: border-box;
border-color: rgba(0,0,0,0.6);
transition: all 0.2s ease-in-out;
}After verifying the mask works, the article adds navigation buttons (previous/next) and integrates Ant Design’s Popover to display step content.
Step configuration is defined via the OnBoardingStepConfig interface, allowing a selector, placement, custom render function, and optional callbacks before moving forward or back.
export interface OnBoardingStepConfig {
selector: () => HTMLElement | null;
placement?: TooltipPlacement;
renderContent?: (currentStep: number) => React.ReactNode;
beforeForward?: (currentStep: number) => void;
beforeBack?: (currentStep: number) => void;
}The main OnBoarding component manages the current step state, handles forward/back actions, and renders the mask with the popover only after the mask animation finishes (using isMaskMoving state and animation callbacks).
export const OnBoarding: FC
= (props) => {
const { step = 0, steps, onStepsEnd, getContainer } = props;
const [currentStep, setCurrentStep] = useState
(0);
const [done, setDone] = useState(false);
const [isMaskMoving, setIsMaskMoving] = useState
(false);
// ...logic for back, forward, effects
const renderPopover = (wrapper: React.ReactNode) => {
const config = steps[currentStep];
if (!config) return wrapper;
const content = config.renderContent ? config.renderContent(currentStep) : null;
const operation = (
{currentStep !== 0 && (
上一步
)}
{currentStep === steps.length - 1 ? '我知道了' : '下一步'}
);
return (
isMaskMoving ? wrapper : (
{content}{operation}
} open={true} placement={config.placement}>
{wrapper}
)
);
};
// render mask and portal
const mask = (
setIsMaskMoving(true)}
onAnimationEnd={() => setIsMaskMoving(false)}
container={currentContainerElement}
element={currentSelectedElement!}
renderMaskContent={renderPopover}
/>
);
return createPortal(mask, currentContainerElement);
};Usage in App.tsx shows three button groups with IDs, and the OnBoarding component is supplied a steps array that points to those IDs and provides simple text content for each step.
import { OnBoarding } from './OnBoarding';
import { Button, Flex } from 'antd';
function App() {
return (
Primary Button
Default Button
Dashed Button
{/* other button groups omitted for brevity */}
document.getElementById('btn-group1'), renderContent: () => '神说要有光', placement: 'bottom' },
{ selector: () => document.getElementById('btn-group2'), renderContent: () => '于是就有了光', placement: 'bottom' },
{ selector: () => document.getElementById('btn-group3'), renderContent: () => '你相信光么', placement: 'bottom' },
]}
/>
);
}
export default App;The article also discusses two remaining issues: the mask not disappearing after the tour ends (solved by a done state) and the need to recompute mask style on window resize (handled with a ResizeObserver inside Mask ).
useEffect(() => {
const observer = new ResizeObserver(() => {
const style = getMaskStyle(element, container || document.documentElement);
setStyle(style);
});
observer.observe(container || document.documentElement);
}, []);Finally, animation start/end callbacks are added to Mask to control when the popover should be rendered, preventing position flicker.
All source code is available in the GitHub repository: https://github.com/QuarkGluonPlasma/react-course-code/tree/main/onboarding-component.
In summary, the tutorial shows how to build a custom onboarding tour component in React using Ant Design 5, replacing the library’s four‑rect mask with a single div mask, handling step navigation, resize events, and animation timing.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.