react-onboarding-docs
Skill0 starsChanged 5 months agoMITView on GitHub
React patterns for product onboarding flows and documentation sites — multi-step flows, progress indicators, empty states, contextual hints, code-block components, TOC sidebars, on-this-page nav, callouts, copy-buttons, search, syntax highlighting. Use when building onboarding UI, product tours, documentation pages, knowledge bases, or developer-facing reference sites. Triggers on "onboarding", "tour", "walkthrough", "empty state", "tooltip", "documentation", "docs site", "code block", "TOC", "table of contents", "callout".
In this repository20 other files it gives its agents.
- react-a11ySkill
- react-api-consumerSkill
- react-bootstrapSkill
- react-brief-recipesSkill
- react-component-designSkill
- react-docs-writingSkill
- react-embedsSkill
- react-forms-validationSkill
- react-html-platformSkill
- react-interview-async-slackSkill
- react-interview-narrationSkill
- react-marketing-patternsSkill
- react-modern-reactSkill
- react-performanceSkill
- react-resourcesSkill
- react-routingSkill
- react-senior-uxSkill
- react-stylingSkill
- react-testingSkill
- react-utility-snippetsSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 437
Search input has a real label (<label className="sr-only">)
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
react-onboarding-docs
Patterns for the two Netlify-scope surfaces beyond marketing pages: onboarding flows (first-run experiences, product tours, empty states) and documentation (code-heavy reference pages, callouts, navigation). Both are about getting a user to "aha" fast — onboarding through guided action, docs through scannable answers.
The shared principle: progressive disclosure
Both onboarding and docs fail when they show everything at once. Show what's relevant now, hide the rest behind a click or scroll.
- Onboarding: one step at a time, each with one clear next action
- Docs: TOC + collapsible sections + "show more" code blocks, not 8000-word walls
Part 1: Onboarding patterns
Multi-step onboarding flow
type Step = {
id: string;
title: string;
description?: string;
render: () => ReactNode;
};
export function OnboardingFlow({ steps, onComplete }: { steps: Step[]; onComplete: () => void }) {
const [index, setIndex] = useState(0);
const step = steps[index];
const isLast = index === steps.length - 1;
return (
<section aria-labelledby="onboarding-title">
<ProgressBar current={index + 1} total={steps.length} />
<header className="mt-6">
<p className="text-sm font-medium text-fg-muted">
Step {index + 1} of {steps.length}
</p>
<h2 id="onboarding-title" className="mt-1 text-2xl font-bold">
{step.title}
</h2>
{step.description && (
<p className="mt-2 text-fg-muted">{step.description}</p>
)}
</header>
<div className="mt-8">{step.render()}</div>
<footer className="mt-8 flex items-center justify-between">
<button
type="button"
onClick={() => setIndex(i => Math.max(0, i - 1))}
disabled={index === 0}
className="text-fg-muted disabled:opacity-50"
>
Back
</button>
<button
type="button"
onClick={() => (isLast ? onComplete() : setIndex(i => i + 1))}
className="inline-flex items-center justify-center h-10 px-5 rounded-md bg-brand text-white font-medium hover:bg-brand-hover"
>
{isLast ? 'Finish' : 'Continue'}
</button>
</footer>
</section>
);
}Progress bar (a11y-aware)
export function ProgressBar({ current, total }: { current: number; total: number }) {
const percent = Math.round((current / total) * 100);
return (
<div
role="progressbar"
aria-valuenow={current}
aria-valuemin={1}
aria-valuemax={total}
aria-label={`Step ${current} of ${total}`}
className="h-1.5 w-full rounded-full bg-fg/10 overflow-hidden"
>
<div
className="h-full bg-brand transition-all duration-300 ease-out"
style={{ width: `${percent}%` }}
/>
</div>
);
}Senior moves:
role="progressbar"+aria-valuenow/min/maxso screen readers announce progressaria-labeldescribes the meaning, not the visual- Animate
widthis fine here — it's a one-shot transition, not a per-frame animation, and it's a single element
Empty state (the "we haven't shown you the product yet" moment)
type EmptyStateProps = {
illustration?: ReactNode;
title: string;
description: string;
primaryAction?: { label: string; onClick: () => void };
secondaryAction?: { label: string; href: string };
};
export function EmptyState({
illustration,
title,
description,
primaryAction,
secondaryAction,
}: EmptyStateProps) {
return (
<section className="flex flex-col items-center text-center py-16 px-6">
{illustration && (
<div aria-hidden="true" className="mb-6 text-fg-muted">
{illustration}
</div>
)}
<h3 className="text-xl font-semibold">{title}</h3>
<p className="mt-2 max-w-md text-fg-muted">{description}</p>
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
{primaryAction && (
<button
type="button"
onClick={primaryAction.onClick}
className="inline-flex h-10 px-5 rounded-md bg-brand text-white font-medium hover:bg-brand-hover"
>
{primaryAction.label}
</button>
)}
{secondaryAction && (
<a
href={secondaryAction.href}
className="text-brand hover:underline underline-offset-4"
>
{secondaryAction.label}
</a>
)}
</div>
</section>
);
}Senior frame for empty states: an empty state is not an error state. It's the highest-value real estate in your product — it's the user's first instruction. Treat it like a hero, not a "no data" placeholder.
Bad: "No projects yet." Good: "Connect your first site in under a minute. We'll guide you through linking a Git repo and your first deploy."
Contextual hint / tooltip (built on <button> semantics)
export function HintTooltip({ children, label }: { children: ReactNode; label: string }) {
const [open, setOpen] = useState(false);
const id = useId();
return (
<span className="relative inline-block">
<button
type="button"
aria-describedby={open ? id : undefined}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
onKeyDown={e => e.key === 'Escape' && setOpen(false)}
className="inline-flex items-center justify-center h-5 w-5 rounded-full bg-fg/10 text-fg-muted hover:bg-fg/20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
>
<span aria-hidden="true">?</span>
<span className="sr-only">{label}</span>
</button>
{open && (
<span
id={id}
role="tooltip"
className="absolute left-1/2 top-full mt-2 -translate-x-1/2 z-10 w-max max-w-xs rounded-md bg-fg text-bg text-sm px-3 py-2 shadow-lg"
>
{children}
</span>
)}
</span>
);
}Senior notes:
- Trigger is
<button>— never just a<span>with mouseenter. Keyboard users need it too. - Opens on hover AND focus; closes on blur AND Escape
aria-describedbywires the tooltip content to the trigger- For production: use Radix UI
Tooltipor React Aria — they handle delay, positioning, and edge cases (mobile, RTL)
"What's new" dismissible banner
export function ChangelogBanner({ id, title, href, onDismiss }: ChangelogBannerProps) {
return (
<aside
role="status"
aria-labelledby={`banner-${id}`}
className="flex items-start gap-3 rounded-lg border border-border bg-bg p-4"
>
<SparkIcon aria-hidden="true" className="mt-0.5 h-5 w-5 text-brand flex-shrink-0" />
<div className="flex-1 min-w-0">
<p id={`banner-${id}`} className="font-medium">
{title}
</p>
<a href={href} className="mt-1 inline-block text-sm text-brand hover:underline">
See what's new →
</a>
</div>
<button
type="button"
onClick={onDismiss}
aria-label="Dismiss"
className="text-fg-muted hover:text-fg p-1 -m-1"
>
<XIcon aria-hidden="true" className="h-4 w-4" />
</button>
</aside>
);
}Persist dismissal in localStorage keyed by the banner ID so it stays dismissed.
Part 2: Documentation site patterns
Code block with copy button (the bread-and-butter docs component)
'use client';
import { useState } from 'react';
type CodeBlockProps = {
code: string;
language?: string;
filename?: string;
highlightLines?: number[];
};
export function CodeBlock({ code, language, filename, highlightLines }: CodeBlockProps) {
const [copied, setCopied] = useState(false);
const copy = async () => {
await navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<figure className="rounded-lg overflow-hidden border border-border bg-fg/[0.03]">
{filename && (
<figcaption className="flex items-center justify-between px-4 py-2 border-b border-border bg-fg/[0.04]">
<code className="text-sm text-fg-muted">{filename}</code>
{language && (
<span className="text-xs uppercase tracking-wide text-fg-muted">
{language}
</span>
)}
</figcaption>
)}
<div className="relative">
<pre
tabIndex={0}
aria-label={`Code example${filename ? ` from ${filename}` : ''}`}
className="overflow-x-auto p-4 text-sm font-mono leading-relaxed"
>
<code className={language ? `language-${language}` : undefined}>
{code}
</code>
</pre>
<button
type="button"
onClick={copy}
aria-label={copied ? 'Copied' : 'Copy code'}
aria-live="polite"
className="absolute right-2 top-2 inline-flex items-center gap-1.5 h-8 px-3 rounded-md bg-bg/80 backdrop-blur text-sm text-fg-muted hover:text-fg border border-border opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity"
>
{copied ? (
<>
<CheckIcon aria-hidden="true" className="h-3.5 w-3.5" />
Copied
</>
) : (
<>
<CopyIcon aria-hidden="true" className="h-3.5 w-3.5" />
Copy
</>
)}
</button>
</div>
</figure>
);
}Senior moves:
<figure>+<figcaption>— semantic structure for "labeled media"<pre tabIndex={0}>so keyboard users can scroll long code blocks- Copy button announces state via
aria-live(screen reader users know it worked) - Highlighting via Shiki or Prism — done at build time when possible (zero-runtime cost)
TOC (table of contents) — auto-generated from headings
'use client';
import { useEffect, useState } from 'react';
type Heading = { id: string; text: string; level: 2 | 3 };
export function TOC() {
const [headings, setHeadings] = useState<Heading[]>([]);
const [activeId, setActiveId] = useState<string>('');
useEffect(() => {
const els = Array.from(
document.querySelectorAll<HTMLHeadingElement>('main h2, main h3')
);
setHeadings(
els.map(el => ({
id: el.id,
text: el.textContent ?? '',
level: (el.tagName === 'H2' ? 2 : 3) as 2 | 3,
}))
);
const io = new IntersectionObserver(
entries => {
const visible = entries.filter(e => e.isIntersecting);
if (visible.length > 0) setActiveId(visible[0].target.id);
},
{ rootMargin: '0px 0px -70% 0px' }
);
els.forEach(el => io.observe(el));
return () => io.disconnect();
}, []);
return (
<nav aria-label="On this page" className="text-sm">
<p className="font-semibold mb-3">On this page</p>
<ul className="space-y-2">
{headings.map(h => (
<li key={h.id} className={h.level === 3 ? 'ml-4' : ''}>
<a
href={`#${h.id}`}
aria-current={activeId === h.id ? 'location' : undefined}
className={
activeId === h.id
? 'text-brand font-medium'
: 'text-fg-muted hover:text-fg'
}
>
{h.text}
</a>
</li>
))}
</ul>
</nav>
);
}Senior detail: aria-current="location" is announced as "current location" by screen readers — far better than just visual styling.
Callout / admonition box
type CalloutKind = 'note' | 'tip' | 'warning' | 'danger';
const kindConfig: Record<CalloutKind, { icon: ReactNode; label: string; className: string }> = {
note: { icon: <InfoIcon />, label: 'Note', className: 'border-fg/20 bg-fg/[0.03]' },
tip: { icon: <SparkIcon />, label: 'Tip', className: 'border-brand/30 bg-brand/[0.05]' },
warning: { icon: <WarningIcon />, label: 'Warning', className: 'border-amber-500/30 bg-amber-500/[0.05]' },
danger: { icon: <DangerIcon />, label: 'Danger', className: 'border-red-500/30 bg-red-500/[0.05]' },
};
export function Callout({
kind = 'note',
title,
children,
}: { kind?: CalloutKind; title?: string; children: ReactNode }) {
const c = kindConfig[kind];
return (
<aside
role="note"
aria-label={title ?? c.label}
className={cn('flex gap-3 rounded-lg border p-4 my-6', c.className)}
>
<span aria-hidden="true" className="flex-shrink-0 mt-0.5">
{c.icon}
</span>
<div className="flex-1 min-w-0">
{title && <p className="font-semibold">{title}</p>}
<div className="prose-sm">{children}</div>
</div>
</aside>
);
}Senior detail: color is paired with an icon AND a label — never relies on color alone (a11y requirement).
On-this-page-quick anchor links (heading anchors)
export function AnchorHeading({ as: Tag = 'h2', id, children }: AnchorHeadingProps) {
return (
<Tag id={id} className="group scroll-mt-20">
<a
href={`#${id}`}
aria-label={`Link to ${typeof children === 'string' ? children : 'section'}`}
className="absolute -ml-6 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 text-fg-muted hover:text-brand"
>
#
</a>
{children}
</Tag>
);
}scroll-mt-20 accounts for sticky header so anchored sections don't get hidden under it.
Docs search (UX shape, not implementation)
The pattern most senior:
Cmd+Kopens a modal search overlay- Type-ahead with keyboard nav (arrow up/down, Enter to select, Esc to close)
- Results grouped (Guides, API, Blog)
- Empty state shows popular searches / recent
A11y must-haves:
- Search input has a real label (
<label className="sr-only">) - Results in
role="listbox", items inrole="option" aria-activedescendanttracks the keyboard-highlighted item- Esc closes; focus returns to the trigger
This is one of the highest-effort components to build well. For an interview, mention you'd use Algolia DocSearch or Pagefind rather than rolling search yourself — it's the right tradeoff.
Documentation page skeleton (Next.js App Router shape)
// app/docs/[...slug]/page.tsx
export default function DocPage({ params }) {
const { content, headings, frontmatter } = getDoc(params.slug);
return (
<div className="mx-auto max-w-7xl grid grid-cols-1 lg:grid-cols-[240px_1fr_240px] gap-12 px-6 py-12">
<aside aria-label="Documentation navigation">
<SidebarNav />
</aside>
<main id="main" tabIndex={-1}>
<article className="prose prose-neutral max-w-none">
<header>
<h1>{frontmatter.title}</h1>
{frontmatter.description && (
<p className="lead">{frontmatter.description}</p>
)}
</header>
{content}
</article>
<PrevNext />
</main>
<aside aria-label="On this page" className="hidden lg:block">
<TOC headings={headings} />
</aside>
</div>
);
}Onboarding/docs UX principles (the things to mention)
- One next action per screen — don't make users decide
- Show the user where they are — progress for onboarding, breadcrumbs/TOC for docs
- Optimistic state — assume success, show success, roll back on error
- Don't trap users — always a Back, always a Skip (with consequences explained)
- Skim-readable — bold the verbs, short paragraphs, generous whitespace
- Code is content — copyable, scrollable, syntax-highlighted, accessible
- Search is non-optional for docs — typing 4 characters > clicking 3 categories
- The empty state is the hero — it's the first instruction the user sees
Common mistakes
| Mistake | Fix |
| Modal onboarding with no Skip | Always allow skip; track who skipped to improve later |
| Multi-step form with no progress indicator | Always show "Step N of M" |
| Tooltips on hover only (mouse-only) | Open on hover AND focus, close on blur AND Esc |
| Code blocks with no copy button | Add a copy button; announce success |
| TOC without active section indicator | Use IntersectionObserver + aria-current |
| Callout that conveys urgency by color alone | Always pair color with icon + text label |
Docs page with no <main> landmark | Wrap content in <main id="main"> + skip link |
Narration phrases
- "Empty states are the highest-value real estate in a product — this is the user's first instruction, so I framed it as a hero with one clear next action rather than a 'no data' placeholder."
- "The progress bar uses
role='progressbar'witharia-valuenow/min/maxso screen reader users hear 'step 2 of 4' announced — visual progress alone leaves them behind." - "I used a real
<figure>and<figcaption>for the code block — semantic 'labeled media' instead of a wrapper div." - "The TOC uses IntersectionObserver to mark the active section and
aria-current='location'so screen readers announce it as 'current location'." - "For search, in a real build I'd reach for Algolia DocSearch or Pagefind — rolling search well is a multi-week project, not a multi-hour one."
- "Callouts pair color, icon, and label — color alone fails a11y and the print-out version."
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

