Skip to main content

What is headless UI? A guide for onboarding engineers

Learn what headless UI means for product tours and onboarding. See code examples and find out why headless beats styled libraries for design system teams.

DomiDex
DomiDexCreator of Tour Kit
April 8, 202610 min read
Share
What is headless UI? A guide for onboarding engineers

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/react

This 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.

DimensionStyled (React Joyride)Headless (Tour Kit)
Bundle size (gzipped)37KBCore <8KB, React <12KB
Match design systemOverride 12+ CSS selectorsUse your own components
Dark mode supportCustom stylesheet requiredWorks automatically (your components handle it)
TypeScript coveragePartial (@types package)Full (written in TypeScript)
React 19 supportClass components internallyHooks only, React 18/19 native
AccessibilityBasic ARIAWCAG 2.1 AA (focus trap, keyboard nav, screen reader)
AI codegen compatibilityStyle conflicts with generated codeNo 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-describedby linking each step to its target element
  • Focus trap within the active step, with restoration on dismiss
  • Escape to close, arrow keys to navigate between steps
  • role="dialog" with proper labeling on each step
  • prefers-reduced-motion respected 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

LibraryApproachHeadlessTypeScriptReact 19
Tour KitHooks + composable packagesYesFullYes
OnboardJSState machine + React bindingsYesYesYes
React JoyrideStyled tooltipsNo (tooltipComponent override)PartialPartial
Shepherd.jsStyled, vanilla JSNoYesWrapper
Intro.jsStyled overlayNoNoNo

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.

Ready to try userTourKit?

$ pnpm add @tour-kit/react