agentsclimarketplace

React onboarding docs

Skill jaballer/react-claude-skills/react-onboarding-docs

21 composable Claude Code skills for senior React + UX engineering — component design, a11y, performance, forms, routing, marketing & docs patterns, testing, and interview craft.

Install
npx -y skills add jaballer/react-claude-skills --skill react-onboarding-docs

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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

SKILL.md

17.8 KB, as published. Nobody here has run it

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/max so screen readers announce progress
  • aria-label describes the meaning, not the visual
  • Animate width is 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-describedby wires the tooltip content to the trigger
  • For production: use Radix UI Tooltip or 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+K opens 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 in role="option"
  • aria-activedescendant tracks 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

MistakeFix
Modal onboarding with no SkipAlways allow skip; track who skipped to improve later
Multi-step form with no progress indicatorAlways 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 buttonAdd a copy button; announce success
TOC without active section indicatorUse IntersectionObserver + aria-current
Callout that conveys urgency by color aloneAlways pair color with icon + text label
Docs page with no <main> landmarkWrap 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' with aria-valuenow/min/max so 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."

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.