
What is headless UI? A guide for onboarding engineers
You have a design system. Your product tour library ships its own tooltips, its own colors, its own overlay. Now your onboarding flow looks like it belongs to a different app.
That's the headless UI problem. And if you work on onboarding, you've probably hit it.
npm install @tourkit/core @tourkit/reactThis guide breaks down the headless component pattern, explains why it matters specifically for product tours and onboarding flows, and shows you how to apply it in practice. We built Tour Kit as a headless onboarding library, so we'll use it for code examples. The architectural pattern applies regardless of which tool you pick.
What is a headless UI component?
A headless UI component handles behavior and state without rendering any visual output. It provides the logic (event handling, keyboard navigation, focus management, ARIA attributes) and leaves the HTML and CSS entirely to the developer. Martin Fowler describes the pattern as extracting "all non-visual logic and state management, separating the brain of a component from its looks" (martinfowler.com). As of April 2026, the five major headless React libraries collectively have over 73,000 GitHub stars.
Think of it as the difference between buying furniture and buying a blueprint. A styled component library like MUI or Chakra UI gives you a finished chair. It works, but reupholstering it means fighting the original fabric. A headless library gives you the joints, screws, and ergonomic measurements. You bring your own wood.
In React, headless components are almost always implemented as custom hooks. A useCombobox() hook handles keyboard navigation, filtering, and selection state. You write the <input> and <ul>.
A useTour() hook handles step sequencing, progress tracking, and element targeting. You write the tooltip.
Why headless architecture matters for onboarding
Product tour tools sit at a unique intersection of design and functionality. A tooltip that doesn't match your brand erodes trust during the exact moment you're trying to build it: first-run onboarding. Teams running Tailwind with a custom design system can't afford a tour library that injects its own CSS specificity wars into the page.
We tested both approaches while building Tour Kit. A styled tour library requires overriding nested CSS selectors to change border radius, shadow, or font. A headless tour library renders nothing. You pass your own <Card> component and it just works.
The difference in integration time was roughly 2 hours versus 15 minutes for a team that already had a component library.
Traditional onboarding tools like Intro.js, Shepherd.js, and React Joyride all bundle pre-styled UI. That made sense in 2018 when most React apps used Bootstrap or no design system at all. But the ecosystem shifted. As of April 2026, headless component adoption grew 70% year-over-year, driven largely by shadcn/ui and Radix Primitives (Bitsrc). Product tour libraries didn't keep up.
How headless components evolved in React
The headless pattern didn't start with hooks. It evolved through three distinct phases, each making the separation of behavior and UI cleaner.
Higher-order components (2015-2018)
HOCs wrapped a component to inject props. withRouter() in React Router v4 is the classic example. For onboarding, you might wrap a page component with withTour() to inject step state. The pattern worked but created "wrapper hell," deeply nested component trees that were hard to debug and impossible to type properly.
Render props (2018-2019)
Render props passed a function as children, giving the consumer full control over rendering. Downshift popularized this for comboboxes. React Joyride uses a variant of this pattern today with its tooltipComponent prop. Better than HOCs, but as Paramanantham Harrison noted, render props create "verbose" component trees that obscure the actual markup (LogRocket).
Custom hooks (2019-present)
Hooks solved both problems. A useTour() hook returns state and handlers. No wrappers, no render functions, no indirection. The component reads exactly like what it renders:
// src/components/TourStep.tsx
import { useTour } from '@tourkit/react';
function TourStep() {
const { currentStep, next, back, isActive } = useTour();
if (!isActive || !currentStep) return null;
return (
<div className="rounded-lg border bg-white p-4 shadow-lg">
<h3 className="font-semibold">{currentStep.title}</h3>
<p className="mt-1 text-sm text-gray-600">{currentStep.content}</p>
<div className="mt-3 flex gap-2">
<button onClick={back} className="text-sm text-gray-500">
Back
</button>
<button onClick={next} className="rounded bg-blue-600 px-3 py-1 text-sm text-white">
Next
</button>
</div>
</div>
);
}Every class, every element, every pixel is yours. The hook handles step state, keyboard events, and focus management behind the scenes.
The headless UI pattern applied to product tours
Most headless component articles focus on dropdowns, comboboxes, and modals. But product tours have unique requirements that make headless architecture even more valuable.
A tour engine needs to manage at least five concerns simultaneously: step sequencing, element targeting, overlay rendering, focus trapping between steps, and progress persistence. In a styled library, all five are tangled with the tooltip UI. Change the tooltip? Risk breaking the targeting logic.
In a headless library, these concerns live in hooks and the rendering is a separate layer.
Here's what Tour Kit's headless architecture looks like in practice:
// src/components/OnboardingTour.tsx
import { TourProvider, Tour, useTour } from '@tourkit/react';
const steps = [
{ id: 'welcome', target: '#dashboard-header', title: 'Welcome', content: 'This is your dashboard.' },
{ id: 'sidebar', target: '#nav-sidebar', title: 'Navigation', content: 'Browse sections here.' },
{ id: 'create', target: '#create-button', title: 'Create', content: 'Start your first project.' },
];
function CustomTooltip() {
const { currentStep, next, back, progress, totalSteps } = useTour();
if (!currentStep) return null;
return (
<YourDesignSystemCard>
<YourBadge>{progress} of {totalSteps}</YourBadge>
<h3>{currentStep.title}</h3>
<p>{currentStep.content}</p>
<YourButtonGroup>
<YourButton variant="ghost" onClick={back}>Back</YourButton>
<YourButton variant="primary" onClick={next}>Continue</YourButton>
</YourButtonGroup>
</YourDesignSystemCard>
);
}
function App() {
return (
<TourProvider>
<Tour steps={steps} render={() => <CustomTooltip />} />
<YourApp />
</TourProvider>
);
}YourDesignSystemCard, YourBadge, YourButton: these are your components from your design system. The tour engine doesn't know or care what they look like.
Headless vs styled: a practical comparison
We installed React Joyride (styled) and Tour Kit (headless) in the same Vite 6 + React 19 + TypeScript 5.7 project to compare the developer experience directly.
| Dimension | Styled (React Joyride) | Headless (Tour Kit) |
|---|---|---|
| Bundle size (gzipped) | 37KB | Core <8KB, React <12KB |
| Match design system | Override 12+ CSS selectors | Use your own components |
| Dark mode support | Custom stylesheet required | Works automatically (your components handle it) |
| TypeScript coverage | Partial (@types package) | Full (written in TypeScript) |
| React 19 support | Class components internally | Hooks only, React 18/19 native |
| Accessibility | Basic ARIA | WCAG 2.1 AA (focus trap, keyboard nav, screen reader) |
| AI codegen compatibility | Style conflicts with generated code | No conflicts, generates to your design tokens |
The bundle size difference alone is significant. React Joyride ships at 37KB gzipped, 4.6 times larger than Tour Kit's core. On mobile connections, that's the difference between a tour that appears instantly and one that delays page interaction.
But the real gap is in the design system workflow. With React Joyride, matching your brand means overriding .react-joyride__tooltip, .react-joyride__beacon, and a dozen nested selectors. With Tour Kit, you render a <div> styled however you want. There's nothing to override because there's nothing pre-built.
Accessibility: the headless advantage
Headless components solve accessibility problems that most developers wouldn't implement correctly on their own. Focus trapping, keyboard navigation, ARIA live regions, and screen reader announcements require deep spec knowledge. Headless libraries like React Aria split each component into state, behavior, and rendered output. The behavior layer handles event handlers, accessibility attributes, and platform-specific quirks automatically (Smashing Magazine).
For product tours, accessibility is especially critical. A tour interrupts the user's normal workflow to show modal-like content anchored to specific elements. Missing focus management traps keyboard users. Skip ARIA announcements and screen reader users won't know a tour step appeared. Leave out Escape key handling, and everyone's frustrated.
Tour Kit's headless hooks handle all of this:
aria-describedbylinking each step to its target element- Focus trap within the active step, with restoration on dismiss
Escapeto close, arrow keys to navigate between stepsrole="dialog"with proper labeling on each stepprefers-reduced-motionrespected for all transitions
You still write the JSX. But the accessibility behavior comes from the hook, not from your memory of the WAI-ARIA spec.
The design system test
Here's a simple heuristic: if your app has a design system, you need a headless tour library.
Design systems exist to enforce visual consistency. Every component (buttons, cards, badges, modals) follows the same spacing, color tokens, and typography scale. A styled tour library that injects its own visual language breaks that consistency at exactly the wrong moment: when a new user is forming their first impression of your product.
Teams using shadcn/ui, Radix, Tailwind, or any token-based design system should treat their tour tooltips like any other component: built with the same primitives, styled with the same tokens, maintained in the same codebase.
This also matters for AI-assisted development workflows. Tools like v0 and Cursor generate React + Tailwind + shadcn/ui code. A headless tour library slots into that output naturally because there are no style conflicts to resolve. A styled library introduces a second design vocabulary that the AI didn't account for.
Common mistakes to avoid
Mistake 1: Choosing headless when you don't have a design system. If you're prototyping and don't have reusable components yet, a styled library like React Joyride or Shepherd.js gets you to a working tour faster. Headless pays off when you have components to reuse, not before.
Mistake 2: Rebuilding accessibility from scratch. The point of a headless library is that it handles behavior including accessibility. If you're writing your own focus trap and ARIA attributes instead of using the hook's built-in support, you're doing extra work and probably getting it wrong.
Mistake 3: Over-abstracting the tour UI. A headless tooltip is just a component. Don't create a generic <TourTooltipFactory> with a config object for every possible layout. Write a concrete component for each tour context: onboarding tooltip, feature callout, checklist nudge. Three simple components beat one complex one.
Headless libraries for onboarding in 2026
| Library | Approach | Headless | TypeScript | React 19 |
|---|---|---|---|---|
| Tour Kit | Hooks + composable packages | Yes | Full | Yes |
| OnboardJS | State machine + React bindings | Yes | Yes | Yes |
| React Joyride | Styled tooltips | No (tooltipComponent override) | Partial | Partial |
| Shepherd.js | Styled, vanilla JS | No | Yes | Wrapper |
| Intro.js | Styled overlay | No | No | No |
We built Tour Kit, so take our inclusion here with appropriate skepticism. Every claim in this table is verifiable against each library's GitHub repo and npm page. Tour Kit's real limitation: it requires React developers — there's no visual builder and no mobile SDK. If your team isn't writing React code, Shepherd.js or a SaaS tool like Appcues is a better fit.
For a deeper comparison of headless UI primitive libraries (Radix, React Aria, Base UI, Ark UI, Headless UI), see our headless UI libraries for onboarding roundup.
Key takeaways
- A headless UI component separates behavior from rendering. For tours, that means the engine handles step logic while you write the tooltip.
- The pattern evolved from HOCs to render props to hooks. Hooks made headless components practical enough to go mainstream.
- If your app has a design system, headless onboarding tools avoid the CSS specificity fights that styled libraries create.
- Accessibility comes free with a good headless library. Focus traps, keyboard navigation, and ARIA attributes are handled by the hook layer.
- Headless architecture aligns with where the React ecosystem is heading: AI-generated code, copy-paste components, and design token systems that don't tolerate visual outliers.
FAQ
What does "headless UI" mean in simple terms?
Headless UI means a component provides behavior (click handling, keyboard navigation, state management, accessibility attributes) without rendering any HTML or CSS. Tour Kit's useTour() hook tracks which step is active, manages focus, and handles keyboard events. You write the actual tooltip markup using your own components and styles. The hook is the brain; your JSX is the face.
Is headless UI harder to implement than a styled library?
Slightly more initial setup, yes, because you write the rendering code yourself. But for teams with an existing design system, it's actually faster. You reuse your <Card> and <Button> components instead of overriding a library's CSS. We measured roughly 15 minutes to integrate Tour Kit with a shadcn/ui project versus 2 hours of CSS overrides with a styled alternative.
Do I need a headless tour library if I don't have a design system?
Not necessarily. If you're prototyping or building an MVP without reusable components, a styled library like React Joyride or Shepherd.js gets you to a working product tour faster. Headless pays off when you have components to compose. Once your app has a <Card> and a <Button>, a headless tour library lets you reuse them for tour steps with zero additional styling work.
How does headless UI improve product tour accessibility?
Headless tour libraries handle the hardest accessibility problems at the hook layer: focus trapping within tour steps, keyboard navigation between steps, ARIA live region announcements for screen readers, and prefers-reduced-motion support. Tour Kit ships WCAG 2.1 AA compliant out of the box. You write the HTML structure; the hook injects the correct ARIA attributes and manages focus automatically.
Can I use headless UI components with Tailwind CSS and shadcn/ui?
Yes. Headless components are specifically designed to work with utility-first CSS and copy-paste component libraries. shadcn/ui itself is built on Radix Primitives, a headless library. Tour Kit follows the same pattern: the useTour() hook provides state and handlers, and you render steps using shadcn/ui's Card, Button, and Popover components with Tailwind classes. No style conflicts, no CSS overrides.
Related articles

Web components vs React components for product tours
Compare web components and React for product tours. Shadow DOM limits, state management gaps, and why framework-specific wins.
Read article
Animation performance in product tours: requestAnimationFrame vs CSS
Compare requestAnimationFrame and CSS animations for product tour tooltips. Learn the two-layer architecture that keeps tours at 60fps without jank.
Read article
Building ARIA-compliant tooltip components from scratch
Build an accessible React tooltip with role=tooltip, aria-describedby, WCAG 1.4.13 hover persistence, and Escape dismissal. Includes working TypeScript code.
Read article
How we benchmark React libraries: methodology and tools
Learn the 5-axis framework we use to benchmark React libraries. Covers bundle analysis, runtime profiling, accessibility audits, and statistical rigor.
Read article