├── README.md ├── apps └── website │ ├── components │ ├── onboarding │ │ ├── writing-onboarding-steps.tsx │ │ ├── install-library.tsx │ │ ├── onboarding-data.tsx │ │ ├── thank-you.tsx │ │ ├── onboarding-step-completion.tsx │ │ ├── introduction-step.tsx │ │ ├── give-feedback-step.tsx │ │ ├── onboarding-setup.tsx │ │ ├── onboarding-step.tsx │ │ └── creating-steps.tsx │ ├── subtitle.tsx │ ├── external-link.tsx │ ├── button.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── button.tsx │ │ ├── form.tsx │ │ └── select.tsx │ ├── onboarding-skeleton.tsx │ ├── analytics.tsx │ ├── footer.tsx │ ├── hero.tsx │ ├── demo.tsx │ └── code-block.tsx │ ├── .eslintrc.json │ ├── .env.example │ ├── app │ ├── favicon.ico │ ├── globals.css │ ├── page.tsx │ └── layout.tsx │ ├── postcss.config.js │ ├── lib │ ├── utils.ts │ ├── hooks.ts │ └── analytics.ts │ ├── next.config.mjs │ ├── components.json │ ├── .gitignore │ ├── public │ ├── vercel.svg │ ├── next.svg │ └── onboarding-lib-logo.svg │ ├── tsconfig.json │ ├── tailwind.config.ts │ ├── package.json │ └── README.md ├── pnpm-workspace.yaml ├── docs ├── onboarding-lib-logo.png └── onboarding-lib-banner.png ├── packages └── lib │ ├── tsconfig.json │ ├── vite.config.ts │ ├── eslint.config.js │ ├── src │ ├── hooks.ts │ ├── utils.ts │ ├── types.ts │ └── index.tsx │ ├── package.json │ └── README.md ├── .gitignore ├── turbo.json ├── package.json ├── prettier.config.cjs ├── .github └── workflows │ └── ci.yml └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | packages/lib/README.md -------------------------------------------------------------------------------- /apps/website/components/onboarding/writing-onboarding-steps.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'apps/*' -------------------------------------------------------------------------------- /apps/website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /apps/website/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_POSTHOG_KEY="phc_...." 2 | NEXT_PUBLIC_POSTHOG_HOST="https://eu.posthog.com" 3 | -------------------------------------------------------------------------------- /apps/website/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/useflytrap/onboarding_lib/HEAD/apps/website/app/favicon.ico -------------------------------------------------------------------------------- /docs/onboarding-lib-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/useflytrap/onboarding_lib/HEAD/docs/onboarding-lib-logo.png -------------------------------------------------------------------------------- /docs/onboarding-lib-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/useflytrap/onboarding_lib/HEAD/docs/onboarding-lib-banner.png -------------------------------------------------------------------------------- /apps/website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/website/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /apps/website/components/subtitle.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | 3 | export function Subtitle({ children, ...props }: { children: ReactNode }) { 4 | return ( 5 |
14 | A tiny headless onboarding library with form validation, schema 15 | validation using Zod and persistance with unstorage. 16 |
17 |
18 | Thank you for checking us out, and giving feedback. We really hope to
19 | make this a really solid onboarding library, so people don't have
20 | to write this same flimsy code over and over again. Please{" "}
21 |
29 | We're building Flytrap, a debugging tool that helps you find the
30 | root causes of your production bugs fast. You can{" "}
31 |
43 | To create conversion funnels for your onboarding flows, you can use 44 | the `onStepCompleted` prop as shown below. If the step has 45 | `validateFormFields` values, those will be validated upon calling the 46 | `next` function, after which the callback will be called. 47 |
48 |18 | ONBOARDING_LIB is a onboarding library for React apps, that makes it 19 | easy to implement fully accessible, customizable and persisted 20 | onboarding flows. 21 |
22 |
27 | We use the great{" "}
28 |
44 | ONBOARDING_LIB is built on top of{" "}
45 |
57 | What follows behind the "Next" button is an onboarding flow, 58 | that will walk you though the ONBOARDING_LIB library, and show how we 59 | built this very onboarding. Very meta, right? 60 |
61 |35 | This will be used with `react-hook-form`. 36 |
37 |67 | This will be used with `react-hook-form`. 68 |
69 |75 | This gives you the `Onboarding` wrapper component as well as a `Step` 76 | component with type-safety with `react-hook-form` forms, and the data 77 | in the onboarding. This will be used with `react-hook-form`. 78 |
79 |82 | As you can see, the above code is the code used by this very onboarding 83 | flow. Let's dive deeper into how to define steps. 84 |
85 |
2 |
3 |
167 | {body} 168 |
169 | ) 170 | }) 171 | FormMessage.displayName = "FormMessage" 172 | 173 | export { 174 | useFormField, 175 | Form, 176 | FormItem, 177 | FormLabel, 178 | FormControl, 179 | FormDescription, 180 | FormMessage, 181 | FormField, 182 | } 183 | -------------------------------------------------------------------------------- /apps/website/components/code-block.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useCallback, useState } from "react" 4 | import copy from "copy-to-clipboard" 5 | import { AnimatePresence, MotionConfig, motion } from "framer-motion" 6 | import { Highlight } from "prism-react-renderer" 7 | import useMeasure from "react-use-measure" 8 | import { twMerge } from "tailwind-merge" 9 | 10 | const variants = { 11 | visible: { opacity: 1, scale: 1 }, 12 | hidden: { opacity: 0, scale: 0.5 }, 13 | } 14 | 15 | const theme = { 16 | plain: { 17 | color: "var(--gray12)", 18 | fontSize: 12, 19 | fontFamily: "var(--font-mono)", 20 | }, 21 | styles: [ 22 | { 23 | types: ["comment"], 24 | style: { 25 | color: "var(--gray9)", 26 | }, 27 | }, 28 | { 29 | types: ["atrule", "keyword", "attr-name", "selector"], 30 | style: { 31 | color: "var(--gray10)", 32 | }, 33 | }, 34 | { 35 | types: ["punctuation", "operator"], 36 | style: { 37 | color: "var(--gray9)", 38 | }, 39 | }, 40 | { 41 | types: ["class-name", "function", "tag"], 42 | style: { 43 | color: "var(--gray12)", 44 | }, 45 | }, 46 | ], 47 | } 48 | 49 | export const CodeBlock = ({ 50 | children, 51 | initialHeight = 0, 52 | copyFromAnywhere, 53 | copyContent, 54 | }: { 55 | children: string 56 | initialHeight?: number 57 | copyFromAnywhere?: true 58 | copyContent?: string 59 | }) => { 60 | const [ref, bounds] = useMeasure() 61 | const [copying, setCopying] = useState(false) 62 | 63 | const onCopy = useCallback(() => { 64 | copy(copyContent ?? children) 65 | setCopying(true) 66 | setTimeout(() => { 67 | setCopying(false) 68 | }, 2000) 69 | }, [copyContent, children]) 70 | 71 | return ( 72 |106 | Each step can be defined with different options. 107 |
108 |114 | We use a `render` prop to render the onboarding steps. Here's the 115 | props that get passed to the `render` function. 116 |
117 |123 | By default, the form only gets submitted when a submit button is 124 | clicked, and not between step transitions. To validate certain form 125 | fields, you can pass in an array to `validateFormFields` prop, with 126 | the keys of the fields you want to validate. 127 |
128 |
134 | For marking a step as completed, or optionally running side-effects
135 | before letting the user continue, the `markAsCompleted` prop can be
136 | used. Your onboarding form data will get passed to the function, and
137 | you can use it to run async side-effects, or just validate the
138 | existance of form data.
139 |
140 | In the below example, the step only gets marked as completed, once the
141 | user has said that they would be "very-disappointed" without
142 | ONBOARDING_LIB ;)
143 |