Menu bar onboarding
NewThe four-step welcome of a menu-bar capture app, each step acted out by a small desktop that loops above it.
menubar-onboarding/menubar-onboarding.tsxThe live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- The first run of a desktop or menu-bar app, where a few settings matter before the first capture.
- Three to five steps, each one idea with a picture of it at work: what the app does, how to call it, how it behaves.
- Choices people can make in a glance (a shortcut, two switches, a few folders) and change later in the settings.
- A welcome that should teach by showing: each step’s illustration acts out what its words say, and follows the choices made.
When not to use
- Settings people return to. Put them in a settings window with sections; the onboarding only starts them.
- Required account data, payment or permissions that block the app. Use a form with validation and clear errors.
- One tip about one feature. Use a popover or a coach mark pointing at it.
- More than five steps, or steps that branch on earlier answers: the panel moves in a straight line with Back and Continue.
- A web page’s sign-up flow: the panel is drawn as a native app’s window, at its fixed 448 × 600.
You can add this component with the shadcn CLI or by hand:
Into components/menubar-onboarding of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/menubar-onboarding.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
menubar-onboarding/menubar-onboarding.tsxmenubar-onboarding/menubar-onboarding.module.cssmenubar-onboarding/hero.tsxmenubar-onboarding/timelines.tsmenubar-onboarding/art.tsxmenubar-onboarding/hero.module.cssmenubar-onboarding/icons.tsxmenubar-onboarding/demo-pointer.tsxmenubar-onboarding/use-autoplay.ts
- Import it
As shown under Usage, below.
Either way it needs React 19.2 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
Usage
import { MenubarOnboarding } from './menubar-onboarding';export function Welcome() { return ( <MenubarOnboarding appName="Clipnest" onFinish={(choices) => saveSettings(choices)} autoplay={false} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
A 448 × 600 panel over a warm desktop: welcome, shortcuts, selection and folders, with Back, Skip for now and Continue. Above each step a miniature desktop plays what it means: the app opens from the menu bar, a window is selected and captured, its thumbnail goes to the canvas to get a backdrop; the shortcut is pressed key by key; the pointer finds areas by itself; a capture drops into a folder. The choices made below (a recorded shortcut, the switches, the folders picked) change what the little desktop shows.
- The illustrations are one desktop drawn in its own px, 448 wide (wallpaper, menu bar, the window, the app’s popover, a thumbnail), seen through a camera that moves and zooms over it, plus the desktop app’s canvas laid on top in the first step. The whole is scaled to the panel’s width, so a narrow panel shows the same picture smaller.
- Each step has a timeline: for every moving part, what it looks like second by second, played by the Web Animations API as one looping animation per part, all started on the same frame. Only transforms and opacity move (a clip-path for the lit area and the canvas’s cut capture), so the compositor plays them.
- The first step runs 16 s: the camera zooms on the menu bar, the pointer opens the popover and clicks Capture, the screen dims and a selection is dragged around the window, a thumbnail slides up in the corner and Edit opens the canvas, where the capture gets a backdrop and then a layout. Step 2 loops every 5.6 s, step 3 every 5.4 s and the drop into a folder every 4.8 s.
- A selection is four edges and four handles, each moved by its own transform, so it can grow from a corner without being redrawn. The area found in step 3 is lit through the dimmed screen by a copy of the window clipped to it.
- Changing step swaps the content at once: the old content is gone in a frame and the new one fades in 70 ms later over 90 ms, as the illustration jumps to the new step’s framing and starts its loop.
- The shortcut buttons record like the system’s: press one, then a key with a modifier; Escape cancels. The capture shortcut is the one the keys of step 2 press. Turning automatic selection off makes the illustration’s pointer drag by hand, and without enhanced detection only bigger areas are found. Unticked folders leave the popover, and new ones can be named.
- While on screen and untouched, a demo pointer goes through the steps at the pace of the reference, clicking Continue (a ring marks each click) and at the end Get started, and the panel starts over. Any real input hands it to the reader; the illustrations keep looping while the panel is visible and pause when it is not.
- Every colour is a light-dark() token: at night the panel turns charcoal, the primary button light, and the desktop a dusk.
| Prop | Type | Default | Description |
|---|---|---|---|
appName | string | 'Clipnest' | The app’s name, in the welcome’s title and text. |
defaultStep | number | 0 | The step it opens on, from 0 (welcome) to 3 (folders). Any other step turns the demo off. |
onFinish | (choices: OnboardingChoices) => void | — | Called on Get started or Skip for now with the choices made: the two shortcuts, the selection switches, the folders picked and whether AI organizes. The panel then starts over. |
autoplay | boolean | true | While on screen and untouched, a demo pointer goes through the steps and clicks Continue. |
colorScheme | 'light' | 'dark' | 'system' | — | Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one. |
defaultState | OnboardingState | — | Opens with choices already made: shortcuts, autoSelection, enhancedDetection, the folders ticked by name (new names are added), organizeWithAI; and recording (a shortcut button waiting for keys) or naming (the new-folder field open). Turns the demo off. |
still | boolean | false | Holds each step’s illustration on one still moment, as under reduced motion. Turns the demo off. |
className | string | — | Extra class for the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useEffectEvent, useId, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type ReactNode,} from 'react';import { FolderGlyph, type FolderKind, type HeroFolder } from './art';import { DemoPointer, runDemo, type Point } from './demo-pointer';import { Hero } from './hero';import { CameraIcon, ChevronLeftIcon, EditIcon, FolderIcon, PlusIcon } from './icons';import { useAutoplay } from './use-autoplay';import styles from './menubar-onboarding.module.css';/* * Menu bar onboarding: the four-step welcome panel of a menu-bar capture app, hanging over its * desktop. Welcome, shortcuts, selection, folders; Back, Skip for now, Continue, and Get started * on the last step./* Menu bar onboarding (menubar-onboarding.tsx): a 448 × 600 panel on a warm desktop wallpaper. The panel is a sheet (the illustration and the step's content) over a grey footer that holds Skip for now and the primary button. Sizes are the panel's real ones, in px; under 480 px the panel takes the stage's width, its illustration scales down and its height follows its content. Colours are the tokens below, light-dark(light, dark): the component takes the color-scheme it inherits from the page, or the one its colorScheme prop sets (data-color-scheme). Memory. The stage is one layout boundary sized from outside. Only transforms and opacity move: the content's fade-in on a step change, the knobs of the switches, the demo pointer and its click ring. The wallpaper is gradients, painted once. */.root { --wall-1: #f8c84c; --wall-2: #f5a532; --wall-3: #ef8426; --wall-4: #e25a29; --wall-5: #cc3c1d; --wall-deep: #ac1d19; --wall-sun: #ffe08a; --wall-blue: #3a86d2; --sheet: #fdfdfd; --footer: #f5f5f5; --ink: #0d0d0d;// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useLayoutEffect, useRef } from 'react';import { Canvas, CapturePill, CapturesPopover, FoldersPopover, Keycaps, MenuBar, MiniWindow, Selection, Thumbnail, type HeroFolder,} from './art';import { MiniArrow } from './icons';import { timeline, type Timeline } from './timelines';import styles from './hero.module.css';/* * The illustration at the top of the panel: a small desktop that acts out each step. */* * The illustrations' timelines: for each step, what every moving part of the desktop does, second * by second, as keyframes the Web Animations API plays on a loop. A part is found by its data-a * name in the hero. Every value is a transform or an opacity (or, for two small parts, a clip-path * and a background), so the compositor plays them. * * Coordinates are the desktop's own px: the desktop is drawn 448 px wide with the window at * (112, 51), 196 × 134, and a camera (the "camera" part) moves and zooms over it. The canvas of the * desktop app is drawn over the desktop, in the hero's px. */export type Frame = [number, Keyframe];export type Track = { /** The data-a name of the part (several parts may share it). */ a: string; /** Times in seconds from the start of the loop, each with what the part looks like then. */ frames: Frame[]; /** The loop's length in seconds; the step's own by default. */ cycle?: number; /** Seconds before the first loop starts. */ delay?: number; /** How many times it plays; for ever by default. */ iterations?: number;};// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { ReactNode } from 'react';import { AppMark, BatteryIcon, BriefcaseIcon, BulbIcon, CameraIcon, EditIcon, EyeIcon, FolderHeartIcon, FolderIcon, GearIcon, HomeIcon, ReturnIcon, RingMark, SearchIcon, SwitchesIcon, VideoIcon, WifiIcon,} from './icons';/* The hero's illustrations (hero.tsx, art.tsx): a small desktop in its own px, scaled to the panel by --k (the panel's width over 448). Every part sits at a fixed place on the desktop; the parts that move start where their timeline starts (timelines.ts), hidden until it shows them. Colours are light-dark() tokens: at night the desktop is a dusk and its windows dark. Memory. The hero is one clipped box. The camera, the canvas and each moving part are moved and faded by transforms and opacity only; the wallpaper is gradients, painted once. */.hero { --wall-1: light-dark(#f7d050, #b8752a); --wall-2: light-dark(#f6b23a, #a9581f); --wall-3: light-dark(#f4a02e, #8f3f1d); --wall-4: light-dark(#e2643f, #6a2620); --wall-5: light-dark(#d4431f, #4d1c22); --wall-sun: light-dark(#ffe58a, #e7a64a); --wall-rim: light-dark(#fbe27a, #c99a4c); --wall-sheen: light-dark(#f9f07c, #d9a24a); --wall-sky: light-dark(#5fbae6, #3d6796); --wall-blue: light-dark(#3a86c8, #24497f); --menubar: light-dark(rgb(255 255 255 / 0.55), rgb(30 30 32 / 0.55)); --menu-ink: light-dark(#191919, #f1f1f1); --win: light-dark(#fafafa, #2b2b2e); --win-edge: light-dark(rgb(0 0 0 / 0.06), rgb(255 255 255 / 0.08)); --win-line: light-dark(#ebebeb, #3a3a3d);// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { ReactNode } from 'react';/* * The line icons of the panel and its illustrations: 24-unit drawings stroked in currentColor. * Their size comes from the class they are given (the lab draws an unsized SVG at 16 px), and * they are decorative: whatever they stand for is said in words next to them. */function Icon({ className, children, fill,}: { className?: string; children: ReactNode; fill?: boolean;}) { return ( <svg className={className}'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it (the eye of the * badge, which plays its own animation on the same clock). The arrow and the hand are HTML boxes * that swap by their visibility: a change of style inside an SVG would lay the SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = { /** Resolves after `ms`. */ wait: (ms: number) => Promise<void>;'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. `visible` and `reduced` are returned too: the * illustrations loop while the panel is visible, whoever is using it, and hold still under reduced * motion. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver(- The panel is a labelled section (its title names it); each step’s change is announced in a polite live region (“Step 2 of 4: Choose your shortcuts”). The illustrations are decorative and hidden from screen readers.
- Back, Skip for now, Continue and Get started are buttons. The shortcut buttons are named with their row and their keys in words (“Capture image, Command Shift 4”) and say how to record; a new shortcut is announced.
- The switches are buttons with role="switch" and aria-checked, labelled by their title and described by their text; Enhanced detection is aria-disabled while automatic selection is off. The folders are toggle buttons with aria-pressed, and the new folder field has a label.
- Every control shows a focus ring. Under reduced motion the illustrations hold a still moment of each step, the content swaps without a fade and the demo never plays.