# V carousel 8

> Onboarding Steps

A shadcn/ui block from the [@cnippet](https://ui.cnippet.dev/) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cnippet/v-carousel-8).

- Page, with a live preview: https://shadcn.exchange/r/cnippet/v-carousel-8
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/cnippet/v-carousel-8-light-825c02f2d3bf7f28.png
- Registry item: https://ui.cnippet.dev/r/v-carousel-8.json
- Categories: carousel
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.cnippet.dev/r/v-carousel-8.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@cnippet/carousel`

## Files

### `registry/default/variants/v-carousel-8.tsx`

```tsx
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";

import useEmblaCarousel from "embla-carousel-react";
import {
  CheckIcon,
  CodeIcon,
  LayersIcon,
  PaletteIcon,
  RocketIcon,
  ZapIcon,
} from "lucide-react";
import * as React from "react";
import { Button } from "@/registry/default/ui/button";

const STEPS = [
  {
    desc: "Let's get your workspace set up in just a few steps.",
    icon: RocketIcon,
    title: "Welcome aboard",
  },
  {
    desc: "Pick a color palette that matches your brand identity.",
    icon: PaletteIcon,
    title: "Choose a theme",
  },
  {
    desc: "Select the UI building blocks your project needs.",
    icon: LayersIcon,
    title: "Pick your components",
  },
  {
    desc: "Run a single command to add components to your project.",
    icon: CodeIcon,
    title: "Install via CLI",
  },
  {
    desc: "Start building — your design system is ready to ship.",
    icon: ZapIcon,
    title: "You're all set!",
  },
];

export default function Particle() {
  const [emblaRef, emblaApi] = useEmblaCarousel({ watchDrag: false });
  const [current, setCurrent] = React.useState(0);

  React.useEffect(() => {
    if (!emblaApi) return;
    const onSelect = () => setCurrent(emblaApi.selectedScrollSnap());
    emblaApi.on("select", onSelect);
    return () => {
      emblaApi.off("select", onSelect);
    };
  }, [emblaApi]);

  const isLast = current === STEPS.length - 1;

  return (
    <div className="w-full max-w-sm space-y-4 rounded-xl border bg-background p-6">
      <div className="space-y-1.5">
        <div className="flex items-center justify-between text-muted-foreground text-xs">
          <span>Getting started</span>
          <span>
            Step {current + 1} of {STEPS.length}
          </span>
        </div>
        <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
          <div
            className="h-full rounded-full bg-primary transition-all duration-300"
            style={{ width: `${((current + 1) / STEPS.length) * 100}%` }}
          />
        </div>
      </div>

      <div className="overflow-hidden" ref={emblaRef}>
        <div className="flex">
          {STEPS.map((step, i) => {
            const Icon = step.icon;
            return (
              <div className="min-w-0 shrink-0 grow-0 basis-full" key={i}>
                <div className="flex flex-col items-center gap-3 py-6 text-center">
                  <div className="flex size-14 items-center justify-center rounded-full bg-primary/10">
                    <Icon aria-hidden="true" className="size-7 text-primary" />
                  </div>
                  <div>
                    <p className="font-semibold">{step.title}</p>
                    <p className="mt-1 text-muted-foreground text-sm">
                      {step.desc}
                    </p>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      <div className="flex gap-2">
        <Button
          className="flex-1"
          disabled={current === 0}
          onClick={() => emblaApi?.scrollPrev()}
          variant="outline"
        >
          Back
        </Button>
        <Button
          className="flex-1"
          onClick={() => !isLast && emblaApi?.scrollNext()}
        >
          {isLast ? (
            <>
              <CheckIcon />
              Finish
            </>
          ) : (
            "Next"
          )}
        </Button>
      </div>
    </div>
  );
}
```
