Skip to content
Nizivo UI

Onboarding page

A new store's home inside the app shell, with a setup checklist and its progress, an empty state for orders and next-step cards. It ships no JavaScript.

From Figma 08 Overview Dashboard, Pattern 8 · Onboarding (7275:16478). Uses PageHeader, Split, Card, ProgressBar, Text, Button, EmptyState and NumberValue.

The example is the page's content only: in the app it goes inside AppShell, which owns the sidebar, the top bars and <main>.

// app/(store)/layout.tsx: the shell wraps every store page; the page renders its content
import { AppShell } from '@nizivo/react/app-shell';

<AppShell sidebar={sidebar} topBar={topBar} mobileTopBar={mobileTopBar} tabBar={tabBar}>
  <OnboardingPageExample />
</AppShell>;

Example

Onboarding

Figma 08's Pattern 8, Onboarding: the content of the app shell's <main> on a new store's home. The page header, then the setup guide (an ordered list of steps, each done or to do in words, with the overall progress on a ProgressBar), an empty state for the orders that have not come yet, and an aside of next-step cards. The neutral blocks stand in for Figma's 3D renders, an Image in the app. A server composition: it ships no JavaScript.

Let's get Moss & Kiln selling

Setup guide

2 of 5 steps done

Setup progress
  1. Add your first productDone · Stoneware mug is live.
  2. Name your store and add a logoDone · Moss & Kiln, with your logo on every page.
  3. Set up paymentsTo do · Choose how customers pay you.
  4. Add shipping ratesTo do · Set what delivery costs, by region.
  5. Tag products in your postsTo do · Shoppers buy straight from your feed.

No orders yet

Put your store link in your Instagram bio

Followers reach your products in one tap from your profile.

Grow plan

Lower card fees, wholesale prices and five staff accounts.

$29.00 a month

import type { JSX } from 'react';
import { CircleCheck, Circle } from '@nizivo/icons';
import { Button } from '@nizivo/react/button';
import { Card } from '@nizivo/react/card';
import { EmptyState } from '@nizivo/react/empty-state';
import { NumberValue } from '@nizivo/react/number-value';
import { PageHeader } from '@nizivo/react/page-header';
import { ProgressBar } from '@nizivo/react/progress-bar';
import { Split } from '@nizivo/react/split';
import { Text } from '@nizivo/react/text';

interface Step {
  id: string;
  title: string;
  detail: string;
  done: boolean;
  /** The step's action while it is still to do. */
  action?: string;
}

const STEPS: Step[] = [
  {
    id: 'product',
    title: 'Add your first product',
    detail: 'Stoneware mug is live.',
    done: true,
  },
  {
    id: 'brand',
    title: 'Name your store and add a logo',
    detail: 'Moss & Kiln, with your logo on every page.',
    done: true,
  },
  {
    id: 'payments',
    title: 'Set up payments',
    detail: 'Choose how customers pay you.',
    done: false,
    action: 'Set up',
  },
  {
    id: 'shipping',
    title: 'Add shipping rates',
    detail: 'Set what delivery costs, by region.',
    done: false,
    action: 'Add rates',
  },
  {
    id: 'instagram',
    title: 'Tag products in your posts',
    detail: 'Shoppers buy straight from your feed.',
    done: false,
    action: 'Tag posts',
  },
];

export function OnboardingPageExample(): JSX.Element {
  const done = STEPS.filter((step) => step.done).length;
  return (
    <div className="nzp-onboarding-page">
      <PageHeader level={2} title="Let's get Moss & Kiln selling" />
      <Split.Root>
        <Split.Main className="nzp-onboarding-page-column">
          <Card.Root render={<section aria-labelledby="nzp-onboarding-guide" />}>
            <Card.Header>
              <Card.Title id="nzp-onboarding-guide">Setup guide</Card.Title>
              <Card.Description>
                {done} of {STEPS.length} steps done
              </Card.Description>
            </Card.Header>
            <ProgressBar
              label="Setup progress"
              value={done}
              max={STEPS.length}
              valueText={`${String(done)} of ${String(STEPS.length)} steps done`}
            />
            <ol className="nzp-onboarding-page-steps">
              {STEPS.map((step) => (
                <li
                  key={step.id}
                  className={
                    step.done
                      ? 'nzp-onboarding-page-step nzp-onboarding-page-done'
                      : 'nzp-onboarding-page-step'
                  }
                >
                  <span className="nzp-onboarding-page-mark">
                    {step.done ? <CircleCheck /> : <Circle />}
                  </span>
                  <span className="nzp-onboarding-page-text">
                    <Text render={<span />} variant="label-m-strong">
                      {step.title}
                    </Text>
                    <Text render={<span />} tone="secondary">
                      {step.done ? 'Done' : 'To do'} · {step.detail}
                    </Text>
                  </span>
                  {step.action !== undefined && (
                    <Button size="sm" aria-label={`${step.action}: ${step.title}`}>
                      {step.action}
                    </Button>
                  )}
                </li>
              ))}
            </ol>
          </Card.Root>
          <EmptyState
            level={3}
            title="No orders yet"
            media={<span className="nzp-onboarding-page-render" aria-hidden="true" />}
            actions={<Button variant="primary">Share your store</Button>}
          />
        </Split.Main>
        <Split.Aside className="nzp-onboarding-page-column">
          <Card.Root tone="brand" render={<section aria-labelledby="nzp-onboarding-instagram" />}>
            <span className="nzp-onboarding-page-media" aria-hidden="true" />
            <Card.Header>
              <Card.Title id="nzp-onboarding-instagram">
                Put your store link in your Instagram bio
              </Card.Title>
              <Card.Description>
                Followers reach your products in one tap from your profile.
              </Card.Description>
            </Card.Header>
            <Card.Footer>
              <Button variant="primary">Copy store link</Button>
            </Card.Footer>
          </Card.Root>
          <Card.Root tone="inverse" render={<section aria-labelledby="nzp-onboarding-plan" />}>
            <Card.Header>
              <Card.Title id="nzp-onboarding-plan">Grow plan</Card.Title>
              <Card.Description>
                Lower card fees, wholesale prices and five staff accounts.
              </Card.Description>
            </Card.Header>
            <Text render={<p />} className="nzp-onboarding-page-price">
              <Text render={<span />} variant="numeric-l">
                <NumberValue value={29} format={{ style: 'currency', currency: 'USD' }} />
              </Text>{' '}
              a month
            </Text>
            <Card.Footer>
              <Button variant="accent">Try Grow free</Button>
            </Card.Footer>
          </Card.Root>
        </Split.Aside>
      </Split.Root>
    </div>
  );
}

Stylesheet

onboarding-page.css
@layer nz.tokens, nz.themes, nz.reset, nz.base, nz.components, nz.patterns, nz.utilities;

/* Figma 08 Pattern 8 (7275:16478): the header, then Split's two columns, 24 apart: the setup guide
   (steps 14 apart in Figma, 16 here, on the scale) and the empty state; the aside's next-step cards.
   A done step's mark is icon/success; its words say "Done" as well. */
@layer nz.patterns {
  .nzp-onboarding-page {
    display: flex;
    flex-direction: column;
    gap: var(--nz-space-6);
    min-inline-size: 0;
  }

  /* each column is the container its steps query */
  .nzp-onboarding-page-column {
    display: flex;
    flex-direction: column;
    gap: var(--nz-space-6);
    container-type: inline-size;
  }

  .nzp-onboarding-page-steps {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* the mark, the words, then the action: under the words in a narrow column, at the row's end
     from 30rem */
  .nzp-onboarding-page-step {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: var(--nz-space-2) var(--nz-space-4);
    padding-block: var(--nz-space-4);
    border-block-start: var(--nz-border-width-default) solid var(--nz-border-subtle);
  }

  .nzp-onboarding-page-mark {
    display: flex;
    color: var(--nz-icon-secondary);
    font-size: 1.25rem;
  }

  .nzp-onboarding-page-done .nzp-onboarding-page-mark {
    color: var(--nz-icon-success);
  }

  .nzp-onboarding-page-text {
    display: flex;
    flex-direction: column;
    gap: var(--nz-space-0-5);
  }

  .nzp-onboarding-page-step > :last-child:not(.nzp-onboarding-page-text) {
    grid-column: 2;
    justify-self: start;
  }

  @container (width >= 30rem) {
    .nzp-onboarding-page-step {
      grid-template-columns: auto minmax(0, 1fr) auto;
    }

    .nzp-onboarding-page-step > :last-child:not(.nzp-onboarding-page-text) {
      grid-column: 3;
    }
  }

  .nzp-onboarding-page-render {
    display: block;
    inline-size: 8.75rem;
    block-size: 8.75rem;
    border-radius: var(--nz-radius-xl);
    background: var(--nz-bg-muted);
  }

  .nzp-onboarding-page-media {
    display: block;
    block-size: 7rem;
    border-radius: var(--nz-radius-lg);
    background: var(--nz-bg-surface);
  }

  .nzp-onboarding-page-price {
    margin: 0;
  }
}

Notes

  • Heading levels. This site's page already has its <h1>, so the example passes level={2} to PageHeader. In your app, drop level (the title becomes the page's <h1>).
  • The checklist is an <ol>: the steps have an order. Each says "Done" or "To do" in words, and its mark (a check or an empty circle) is decorative. Each action's name includes its step ("Set up: Set up payments"), since several sit in one list.
  • Progress. The ProgressBar's valueText reads "2 of 5 steps done" rather than a percentage.
  • Not built. Figma's Checklist / Setup guide, Insight Card and Plan Card have no component: the guide is composed here, and the next steps are Cards (tone="brand" and tone="inverse"). The neutral blocks stand in for Figma's 3D renders; put an Image there.

On this page