Carousel
Free, copy-and-go Carousel components built on the SevenUI Carousel primitive.Read the primitive docs.
Payment methods
Swipe to pick the card for renewals.
Visa 4242 is your default
"use client";
import { CreditCardIcon } from "lucide-react";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const cards = [
{
id: "visa-4242",
brand: "Visa",
last4: "4242",
holder: "Jordan Ellis",
expires: "08/28",
},
{
id: "mastercard-8210",
brand: "Mastercard",
last4: "8210",
holder: "Jordan Ellis",
expires: "03/27",
},
{
id: "amex-1005",
brand: "American Express",
last4: "1005",
holder: "Ellis Studio LLC",
expires: "11/29",
},
];
export default function Carousel01() {
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
const [defaultId, setDefaultId] = React.useState(cards[0].id);
React.useEffect(() => {
if (!api) return;
const onSelect = () => setCurrent(api.selectedScrollSnap());
onSelect();
api.on("select", onSelect);
api.on("reInit", onSelect);
return () => {
api.off("select", onSelect);
api.off("reInit", onSelect);
};
}, [api]);
const card = cards[current];
const isDefault = card.id === defaultId;
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<div className="flex flex-col gap-0.5">
<h3 id="carousel-01-title" className="font-medium">
Payment methods
</h3>
<p className="text-sm text-muted-foreground">
Swipe to pick the card for renewals.
</p>
</div>
<Carousel
setApi={setApi}
opts={{ align: "start" }}
aria-labelledby="carousel-01-title"
>
<CarouselContent className="-ml-3">
{cards.map((item, index) => (
<CarouselItem
key={item.id}
aria-label={`${index + 1} of ${cards.length}: ${item.brand} ending in ${item.last4}`}
className="basis-[88%] pl-3"
>
<div className="flex aspect-[1.586] flex-col justify-between rounded-xl bg-foreground p-4 text-background shadow-sm">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-semibold">{item.brand}</span>
{item.id === defaultId ? (
<Badge
variant="outline"
className="border-background/30 text-background"
>
Default
</Badge>
) : (
<CreditCardIcon
aria-hidden="true"
className="size-5 opacity-60"
/>
)}
</div>
<div className="flex flex-col gap-2">
<span className="text-lg font-medium tracking-wider tabular-nums">
<span aria-hidden="true">•••• •••• •••• </span>
{item.last4}
</span>
<div className="flex items-end justify-between gap-2 text-xs">
<span className="truncate opacity-80">{item.holder}</span>
<span className="shrink-0 tabular-nums opacity-80">
Exp {item.expires}
</span>
</div>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<div className="mt-3 flex items-center justify-between gap-4">
<div className="flex items-center gap-1.5">
{cards.map((item, index) => (
<button
key={item.id}
type="button"
aria-label={`Show ${item.brand} ending in ${item.last4}`}
aria-current={index === current ? "true" : undefined}
onClick={() => api?.scrollTo(index)}
className="h-1.5 w-1.5 rounded-full bg-muted-foreground/30 transition-[width,background-color] duration-300 ease-out outline-none hover:bg-muted-foreground/60 focus-visible:ring-3 focus-visible:ring-ring/50 aria-current:w-5 aria-current:bg-primary"
/>
))}
</div>
<div className="flex items-center gap-2">
<CarouselPrevious className="static my-0" />
<CarouselNext className="static my-0" />
</div>
</div>
</Carousel>
<div className="flex items-center justify-between gap-3 rounded-lg border px-3 py-2.5">
<p className="min-w-0 text-sm" aria-live="polite">
<span className="font-medium">
{card.brand} {card.last4}
</span>
<span className="text-muted-foreground">
{isDefault ? " is your default" : " selected"}
</span>
</p>
<Button
size="sm"
variant={isDefault ? "ghost" : "default"}
disabled={isDefault}
onClick={() => setDefaultId(card.id)}
>
{isDefault ? "Default card" : "Make default"}
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-01pnpm dlx shadcn@latest add @sevenui/component/carousel-01yarn dlx shadcn@latest add @sevenui/component/carousel-01bunx --bun shadcn@latest add @sevenui/component/carousel-01"use client";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Badge } from "@/components/ui/badge";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const products = [
{ name: "Linen overshirt", price: "$89", tag: "New" },
{ name: "Merino crew sweater", price: "$120", tag: null },
{ name: "Canvas tote bag", price: "$38", tag: "Low stock" },
{ name: "Leather card holder", price: "$45", tag: null },
{ name: "Cotton twill cap", price: "$29", tag: "New" },
{ name: "Wool blend scarf", price: "$64", tag: null },
];
export default function Carousel02() {
return (
<Carousel
opts={{ align: "start" }}
aria-label="Recently viewed"
className="w-full max-w-xl"
>
<div className="mb-3 flex items-center justify-between gap-4">
<h3 className="text-sm font-semibold">Recently viewed</h3>
<div className="flex items-center gap-1">
<CarouselPrevious
variant="ghost"
className="static translate-y-0 rounded-md"
/>
<CarouselNext
variant="ghost"
className="static translate-y-0 rounded-md"
/>
</div>
</div>
<CarouselContent className="-ml-3">
{products.map((product, index) => (
<CarouselItem
key={product.name}
aria-label={`${index + 1} of ${products.length}`}
className="basis-1/2 pl-3 sm:basis-1/3"
>
<a
href={`#${product.name.toLowerCase().replaceAll(" ", "-")}`}
className="group/product flex flex-col gap-2 rounded-lg outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring"
>
<AspectRatio
ratio={4 / 5}
className="overflow-hidden rounded-lg bg-muted"
>
<img
src="/placeholder.svg"
alt={product.name}
className="size-full object-cover transition-transform duration-500 ease-out group-hover/product:scale-105"
/>
{product.tag ? (
<Badge
variant={product.tag === "New" ? "default" : "secondary"}
className="absolute top-2 left-2"
>
{product.tag}
</Badge>
) : null}
</AspectRatio>
<div className="flex items-baseline justify-between gap-2 text-sm">
<span className="min-w-0 truncate font-medium">
{product.name}
</span>
<span className="shrink-0 text-muted-foreground tabular-nums">
{product.price}
</span>
</div>
</a>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
);
}
npx shadcn@latest add @sevenui/component/carousel-02pnpm dlx shadcn@latest add @sevenui/component/carousel-02yarn dlx shadcn@latest add @sevenui/component/carousel-02bunx --bun shadcn@latest add @sevenui/component/carousel-02Activity
api-gateway deployed to production
v2.14.0 rolled out to all 6 regions.
Elevated latency in eu-west-1
p95 rose to 840 ms. Auto-scaling added 3 nodes.
Pull request 1284 merged into main
Rate limiter now reads limits per workspace.
Nightly backup completed
412 GB snapshot verified in 6 min 12 s.
billing-worker deployed to staging
v1.9.3 awaiting QA sign-off.
"use client";
import {
CircleAlertIcon,
CircleCheckIcon,
GitPullRequestIcon,
RocketIcon,
type LucideIcon,
} from "lucide-react";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
type Update = {
icon: LucideIcon;
tone: string;
title: string;
detail: string;
time: string;
};
const updates: Update[] = [
{
icon: RocketIcon,
tone: "bg-primary/10 text-primary",
title: "api-gateway deployed to production",
detail: "v2.14.0 rolled out to all 6 regions.",
time: "2m ago",
},
{
icon: CircleAlertIcon,
tone: "bg-warning/15 text-warning",
title: "Elevated latency in eu-west-1",
detail: "p95 rose to 840 ms. Auto-scaling added 3 nodes.",
time: "18m ago",
},
{
icon: GitPullRequestIcon,
tone: "bg-muted text-muted-foreground",
title: "Pull request 1284 merged into main",
detail: "Rate limiter now reads limits per workspace.",
time: "41m ago",
},
{
icon: CircleCheckIcon,
tone: "bg-success/15 text-success",
title: "Nightly backup completed",
detail: "412 GB snapshot verified in 6 min 12 s.",
time: "3h ago",
},
{
icon: RocketIcon,
tone: "bg-primary/10 text-primary",
title: "billing-worker deployed to staging",
detail: "v1.9.3 awaiting QA sign-off.",
time: "5h ago",
},
];
export default function Carousel03() {
return (
<div className="w-full max-w-sm rounded-xl border bg-card text-card-foreground">
<Carousel
orientation="vertical"
opts={{ align: "start" }}
aria-label="Deployment activity"
>
<div className="flex items-center justify-between gap-4 border-b px-4 py-3">
<h3 className="text-sm font-semibold">Activity</h3>
<div className="flex items-center gap-1">
<CarouselPrevious
variant="ghost"
className="static translate-x-0 rounded-md"
/>
<CarouselNext
variant="ghost"
className="static translate-x-0 rounded-md"
/>
</div>
</div>
<CarouselContent className="mt-0 -mb-px h-44">
{updates.map((update, index) => {
const Icon = update.icon;
return (
<CarouselItem
key={update.title}
aria-label={`${index + 1} of ${updates.length}`}
className="basis-1/2 pt-0"
>
<div className="flex h-full items-start gap-3 border-b px-4 py-3">
<span
className={`flex size-8 shrink-0 items-center justify-center rounded-full ${update.tone}`}
>
<Icon className="size-4" aria-hidden="true" />
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex items-baseline justify-between gap-2">
<p className="truncate text-sm font-medium">
{update.title}
</p>
<time className="shrink-0 text-xs text-muted-foreground tabular-nums">
{update.time}
</time>
</div>
<p className="text-xs text-muted-foreground">
{update.detail}
</p>
</div>
</div>
</CarouselItem>
);
})}
</CarouselContent>
</Carousel>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-03pnpm dlx shadcn@latest add @sevenui/component/carousel-03yarn dlx shadcn@latest add @sevenui/component/carousel-03bunx --bun shadcn@latest add @sevenui/component/carousel-03"use client";
import * as React from "react";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const photos = [
{ alt: "Living room with oak floors and a bay window" },
{ alt: "Open kitchen with a marble island" },
{ alt: "Primary bedroom facing the garden" },
{ alt: "Bathroom with a walk-in shower" },
{ alt: "Home office with built-in shelves" },
{ alt: "Back garden with a stone patio" },
];
export default function Carousel04() {
const [mainApi, setMainApi] = React.useState<CarouselApi>();
const [thumbApi, setThumbApi] = React.useState<CarouselApi>();
const [selected, setSelected] = React.useState(0);
React.useEffect(() => {
if (!mainApi) return;
const onSelect = () => {
const index = mainApi.selectedScrollSnap();
setSelected(index);
thumbApi?.scrollTo(index);
};
onSelect();
mainApi.on("select", onSelect);
mainApi.on("reInit", onSelect);
return () => {
mainApi.off("select", onSelect);
mainApi.off("reInit", onSelect);
};
}, [mainApi, thumbApi]);
return (
<div className="flex w-full max-w-md flex-col gap-3">
<Carousel setApi={setMainApi} aria-label="Listing photos">
<CarouselContent>
{photos.map((photo, index) => (
<CarouselItem
key={photo.alt}
aria-label={`${index + 1} of ${photos.length}`}
>
<AspectRatio
ratio={4 / 3}
className="overflow-hidden rounded-xl bg-muted"
>
<img
src="/placeholder.svg"
alt={photo.alt}
className="size-full object-cover"
/>
</AspectRatio>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-3 border-0 bg-background/80 shadow-sm backdrop-blur-sm" />
<CarouselNext className="right-3 border-0 bg-background/80 shadow-sm backdrop-blur-sm" />
<span
aria-live="polite"
className="absolute right-3 bottom-3 rounded-md bg-background/80 px-2 py-0.5 text-xs font-medium tabular-nums shadow-sm backdrop-blur-sm"
>
{selected + 1} / {photos.length}
</span>
</Carousel>
<Carousel
setApi={setThumbApi}
opts={{ containScroll: "keepSnaps", dragFree: true }}
aria-label="Photo thumbnails"
>
<CarouselContent className="-ml-2">
{photos.map((photo, index) => (
<CarouselItem
key={photo.alt}
className="basis-1/4 pl-2 sm:basis-1/5"
>
<button
type="button"
aria-label={`Show photo ${index + 1}: ${photo.alt}`}
aria-current={index === selected ? "true" : undefined}
onClick={() => mainApi?.scrollTo(index)}
className="block w-full overflow-hidden rounded-md border-2 border-transparent opacity-60 transition-[opacity,border-color] duration-200 outline-none hover:opacity-90 focus-visible:border-ring focus-visible:opacity-100 aria-current:border-primary aria-current:opacity-100"
>
<AspectRatio ratio={1} className="rounded-sm bg-muted">
<img
src="/placeholder.svg"
alt=""
className="size-full object-cover"
/>
</AspectRatio>
</button>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-04pnpm dlx shadcn@latest add @sevenui/component/carousel-04yarn dlx shadcn@latest add @sevenui/component/carousel-04bunx --bun shadcn@latest add @sevenui/component/carousel-04Recommended reading
"use client";
import { RotateCwIcon } from "lucide-react";
import * as React from "react";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Skeleton } from "@/components/ui/skeleton";
const articles = [
{
title: "Designing forms people finish",
source: "Field Notes",
minutes: 8,
},
{
title: "What we learned shipping dark mode twice",
source: "Engineering Blog",
minutes: 12,
},
{
title: "A practical guide to optimistic UI",
source: "Frontend Weekly",
minutes: 6,
},
{
title: "Pricing pages that answer the real question",
source: "Growth Letters",
minutes: 9,
},
];
export default function Carousel05() {
const [loading, setLoading] = React.useState(true);
// Simulates a request: every refresh shows skeletons for a moment.
React.useEffect(() => {
if (!loading) return;
const timeout = window.setTimeout(() => setLoading(false), 1400);
return () => window.clearTimeout(timeout);
}, [loading]);
return (
<section
aria-labelledby="carousel-05-heading"
aria-busy={loading}
className="w-full max-w-md"
>
<div className="mb-3 flex items-center justify-between gap-4">
<h3 id="carousel-05-heading" className="text-sm font-semibold">
Recommended reading
</h3>
<Button
variant="ghost"
size="sm"
disabled={loading}
onClick={() => setLoading(true)}
>
<RotateCwIcon
aria-hidden="true"
data-icon="inline-start"
className={loading ? "animate-spin" : undefined}
/>
{loading ? "Loading" : "Refresh"}
</Button>
</div>
<Carousel opts={{ align: "start" }} aria-label="Recommended reading">
<CarouselContent>
{articles.map((article, index) => (
<CarouselItem
key={article.title}
aria-label={`${index + 1} of ${articles.length}`}
className="basis-4/5 sm:basis-3/5"
>
{loading ? (
<div className="flex flex-col gap-3" aria-hidden="true">
<Skeleton className="aspect-video w-full rounded-lg" />
<div className="flex flex-col gap-2">
<Skeleton className="h-3 w-1/3" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
</div>
</div>
) : (
<article className="flex animate-in flex-col gap-3 duration-300 fade-in-0">
<AspectRatio
ratio={16 / 9}
className="overflow-hidden rounded-lg bg-muted"
>
<img
src="/placeholder.svg"
alt=""
className="size-full object-cover"
/>
</AspectRatio>
<div className="flex flex-col gap-1">
<p className="text-xs text-muted-foreground">
{article.source} · {article.minutes} min read
</p>
<h4 className="text-sm leading-snug font-medium text-balance">
<a
href={`#${article.title.toLowerCase().replaceAll(" ", "-")}`}
className="decoration-2 underline-offset-4 outline-none hover:underline focus-visible:underline focus-visible:decoration-ring"
>
{article.title}
</a>
</h4>
</div>
</article>
)}
</CarouselItem>
))}
</CarouselContent>
<div className="mt-4 flex justify-end gap-2">
<CarouselPrevious
className="static translate-y-0"
{...(loading ? { disabled: true } : {})}
/>
<CarouselNext
className="static translate-y-0"
{...(loading ? { disabled: true } : {})}
/>
</div>
</Carousel>
</section>
);
}
npx shadcn@latest add @sevenui/component/carousel-05pnpm dlx shadcn@latest add @sevenui/component/carousel-05yarn dlx shadcn@latest add @sevenui/component/carousel-05bunx --bun shadcn@latest add @sevenui/component/carousel-05Automate the handoff between design and code
Trigger a review the moment a Figma frame is marked ready.
See where every sprint actually went
Cycle time, scope creep, and carry-over in one weekly digest.
SSO and SCIM on every paid plan
Provision seats from Okta or Entra ID without a support ticket.
Triage your inbox from anywhere
Swipe to assign, snooze, or close issues on iOS and Android.
"use client";
import { PauseIcon, PlayIcon } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
const SLIDE_DURATION = 5000;
const TICK = 100;
const stories = [
{
label: "Workflows",
title: "Automate the handoff between design and code",
body: "Trigger a review the moment a Figma frame is marked ready.",
},
{
label: "Insights",
title: "See where every sprint actually went",
body: "Cycle time, scope creep, and carry-over in one weekly digest.",
},
{
label: "Security",
title: "SSO and SCIM on every paid plan",
body: "Provision seats from Okta or Entra ID without a support ticket.",
},
{
label: "Mobile",
title: "Triage your inbox from anywhere",
body: "Swipe to assign, snooze, or close issues on iOS and Android.",
},
];
export default function Carousel06() {
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
const [elapsed, setElapsed] = React.useState(0);
const [playing, setPlaying] = React.useState(true);
// Hovering or focusing the slides pauses the timer so text can be read.
const [hovered, setHovered] = React.useState(false);
// Respect reduced motion: start paused, the viewer can still press play.
React.useEffect(() => {
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
setPlaying(false);
}
}, []);
React.useEffect(() => {
if (!api) return;
const onSelect = () => {
setCurrent(api.selectedScrollSnap());
setElapsed(0);
};
onSelect();
api.on("select", onSelect);
api.on("reInit", onSelect);
return () => {
api.off("select", onSelect);
api.off("reInit", onSelect);
};
}, [api]);
const running = playing && !hovered;
React.useEffect(() => {
if (!running) return;
const interval = window.setInterval(() => {
setElapsed((value) => Math.min(value + TICK, SLIDE_DURATION));
}, TICK);
return () => window.clearInterval(interval);
}, [running]);
React.useEffect(() => {
if (elapsed >= SLIDE_DURATION) api?.scrollNext();
}, [elapsed, api]);
return (
<Carousel
setApi={setApi}
opts={{ loop: true }}
aria-label="Product updates"
className="w-full max-w-lg overflow-hidden rounded-xl border bg-card text-card-foreground"
>
<CarouselContent
aria-live={running ? "off" : "polite"}
onPointerEnter={() => setHovered(true)}
onPointerLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) {
setHovered(false);
}
}}
>
{stories.map((story, index) => (
<CarouselItem
key={story.title}
aria-label={`${index + 1} of ${stories.length}`}
>
<div className="flex min-h-44 flex-col justify-between gap-6 p-6">
<div className="flex flex-col gap-1.5">
<h3 className="text-lg leading-snug font-semibold text-balance">
{story.title}
</h3>
<p className="text-sm text-pretty text-muted-foreground">
{story.body}
</p>
</div>
<a
href={`#${story.label.toLowerCase()}`}
className="self-start text-sm font-medium text-primary decoration-2 underline-offset-4 outline-none hover:underline focus-visible:underline focus-visible:decoration-ring"
>
Explore {story.label.toLowerCase()}
</a>
</div>
</CarouselItem>
))}
</CarouselContent>
<div className="flex items-center gap-3 border-t px-6 py-3">
<div className="flex flex-1 items-center gap-1.5">
{stories.map((story, index) => {
const fill =
index === current
? (elapsed / SLIDE_DURATION) * 100
: index < current
? 100
: 0;
return (
<button
key={story.title}
type="button"
aria-label={`Go to slide ${index + 1}: ${story.label}`}
aria-current={index === current ? "true" : undefined}
onClick={() => api?.scrollTo(index)}
className="group/segment flex h-6 flex-1 items-center rounded-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
>
<span className="relative h-1 w-full overflow-hidden rounded-full bg-muted group-hover/segment:bg-muted-foreground/25">
<span
className="absolute inset-y-0 left-0 rounded-full bg-primary transition-[width] duration-100 ease-linear"
style={{ width: `${fill}%` }}
/>
</span>
</button>
);
})}
</div>
<Button
variant="ghost"
size="icon-sm"
aria-label={playing ? "Pause autoplay" : "Start autoplay"}
onClick={() => setPlaying((value) => !value)}
>
{playing ? (
<PauseIcon aria-hidden="true" />
) : (
<PlayIcon aria-hidden="true" />
)}
</Button>
</div>
</Carousel>
);
}
npx shadcn@latest add @sevenui/component/carousel-06pnpm dlx shadcn@latest add @sevenui/component/carousel-06yarn dlx shadcn@latest add @sevenui/component/carousel-06bunx --bun shadcn@latest add @sevenui/component/carousel-06Who will use it?
What comes first?
Workspace ready
We set up a starter template for your team. You can change it any time.
"use client";
import {
BuildingIcon,
CircleCheckIcon,
type LucideIcon,
UserIcon,
UsersIcon,
} from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { Progress } from "@/components/ui/progress";
type Choice = {
value: string;
label: string;
hint: string;
summary: string;
icon?: LucideIcon;
};
const teamSizes: Choice[] = [
{
value: "solo",
label: "Just me",
hint: "Personal projects",
summary: "you",
icon: UserIcon,
},
{
value: "team",
label: "2-20 people",
hint: "A single team",
summary: "your team",
icon: UsersIcon,
},
{
value: "org",
label: "20+ people",
hint: "Several teams",
summary: "your organization",
icon: BuildingIcon,
},
];
const goals: Choice[] = [
{
value: "roadmap",
label: "Plan a roadmap",
hint: "Quarters and milestones",
summary: "roadmap",
},
{
value: "bugs",
label: "Track bugs",
hint: "Triage and SLAs",
summary: "bug triage",
},
{
value: "sprints",
label: "Run sprints",
hint: "Cycles and velocity",
summary: "sprint planning",
},
];
const STEP_COUNT = 3;
function ChoiceList({
choices,
value,
onChange,
label,
}: {
choices: Choice[];
value: string | null;
onChange: (value: string) => void;
label: string;
}) {
return (
<fieldset className="flex min-w-0 flex-col gap-2">
<legend className="sr-only">{label}</legend>
{choices.map((choice) => {
const Icon = choice.icon;
return (
<button
key={choice.value}
type="button"
aria-pressed={value === choice.value}
onClick={() => onChange(choice.value)}
className="flex items-center gap-3 rounded-lg border bg-background px-3 py-2.5 text-left transition-colors outline-none hover:bg-muted focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset aria-pressed:border-primary aria-pressed:bg-primary/5"
>
{Icon ? (
<Icon
className="size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
) : null}
<span className="flex min-w-0 flex-1 flex-col">
<span className="text-sm font-medium">{choice.label}</span>
<span className="text-xs text-muted-foreground">
{choice.hint}
</span>
</span>
<span
aria-hidden="true"
className="flex size-4 shrink-0 items-center justify-center rounded-full border in-aria-pressed:border-primary in-aria-pressed:bg-primary"
>
<span className="size-1.5 rounded-full bg-primary-foreground opacity-0 in-aria-pressed:opacity-100" />
</span>
</button>
);
})}
</fieldset>
);
}
export default function Carousel07() {
const [api, setApi] = React.useState<CarouselApi>();
const [step, setStep] = React.useState(0);
const [teamSize, setTeamSize] = React.useState<string | null>(null);
const [goal, setGoal] = React.useState<string | null>(null);
const goTo = (next: number) => {
setStep(next);
api?.scrollTo(next);
};
const reset = () => {
setTeamSize(null);
setGoal(null);
goTo(0);
};
const canContinue =
(step === 0 && teamSize !== null) || (step === 1 && goal !== null);
const teamChoice = teamSizes.find((choice) => choice.value === teamSize);
const goalChoice = goals.find((choice) => choice.value === goal);
return (
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4 text-xs text-muted-foreground">
<span id="carousel-07-heading" className="font-medium text-foreground">
Set up your workspace
</span>
<span className="tabular-nums">
Step {step + 1} of {STEP_COUNT}
</span>
</div>
<Progress
value={((step + 1) / STEP_COUNT) * 100}
aria-labelledby="carousel-07-heading"
/>
</div>
<Carousel
setApi={setApi}
// Steps only advance through the buttons below: dragging and the
// built-in arrow keys are turned off so a step cannot be skipped.
opts={{ watchDrag: false, duration: 20 }}
onKeyDownCapture={() => {}}
aria-labelledby="carousel-07-heading"
>
<CarouselContent>
<CarouselItem
aria-label={`1 of ${STEP_COUNT}`}
inert={step !== 0}
className="flex flex-col gap-3"
>
<h3 className="text-base font-semibold">Who will use it?</h3>
<ChoiceList
label="Team size"
choices={teamSizes}
value={teamSize}
onChange={setTeamSize}
/>
</CarouselItem>
<CarouselItem
aria-label={`2 of ${STEP_COUNT}`}
inert={step !== 1}
className="flex flex-col gap-3"
>
<h3 className="text-base font-semibold">What comes first?</h3>
<ChoiceList
label="Primary goal"
choices={goals}
value={goal}
onChange={setGoal}
/>
</CarouselItem>
<CarouselItem
aria-label={`3 of ${STEP_COUNT}`}
inert={step !== 2}
className="flex flex-col items-center justify-center gap-3 text-center"
>
<span className="flex size-10 items-center justify-center rounded-full bg-success/15 text-success">
<CircleCheckIcon className="size-5" aria-hidden="true" />
</span>
<div className="flex flex-col gap-1">
<h3 className="text-base font-semibold">Workspace ready</h3>
<p className="text-sm text-pretty text-muted-foreground">
We set up a {goalChoice?.summary ?? "starter"} template for{" "}
{teamChoice?.summary ?? "your team"}. You can change it any
time.
</p>
</div>
</CarouselItem>
</CarouselContent>
</Carousel>
<div className="flex items-center justify-between gap-2">
{step === 2 ? (
<>
<Button variant="ghost" onClick={reset}>
Start over
</Button>
<Button>Open workspace</Button>
</>
) : (
<>
<Button
variant="ghost"
disabled={step === 0}
onClick={() => goTo(step - 1)}
>
Back
</Button>
<Button disabled={!canContinue} onClick={() => goTo(step + 1)}>
Continue
</Button>
</>
)}
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-07pnpm dlx shadcn@latest add @sevenui/component/carousel-07yarn dlx shadcn@latest add @sevenui/component/carousel-07bunx --bun shadcn@latest add @sevenui/component/carousel-07Teams that switched this year
“We moved 40 engineers off three separate trackers in a week. Nobody asked to go back.”
“Release notes used to take me a full afternoon. Now they write themselves from merged work.”
“SCIM provisioning was the reason we signed. The keyboard shortcuts are why the team stayed.”
“Audit log export turned our compliance review from a two-week scramble into a single download.”
"use client";
import * as React from "react";
import { cn } from "cn";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
const testimonials = [
{
company: "Brightline",
quote:
"We moved 40 engineers off three separate trackers in a week. Nobody asked to go back.",
name: "Amara Osei",
initials: "AO",
role: "VP Engineering",
},
{
company: "Paperfold",
quote:
"Release notes used to take me a full afternoon. Now they write themselves from merged work.",
name: "Tomás Rivera",
initials: "TR",
role: "Product Manager",
},
{
company: "Kestrel Labs",
quote:
"SCIM provisioning was the reason we signed. The keyboard shortcuts are why the team stayed.",
name: "Hannah Lindqvist",
initials: "HL",
role: "Head of IT",
},
{
company: "Oakridge Health",
quote:
"Audit log export turned our compliance review from a two-week scramble into a single download.",
name: "Samuel Adeyemi",
initials: "SA",
role: "Security Lead",
},
];
export default function Carousel08() {
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
React.useEffect(() => {
if (!api) return;
const onSelect = () => setCurrent(api.selectedScrollSnap());
onSelect();
api.on("select", onSelect);
api.on("reInit", onSelect);
return () => {
api.off("select", onSelect);
api.off("reInit", onSelect);
};
}, [api]);
return (
<section
aria-labelledby="carousel-08-title"
className="flex w-full max-w-xl flex-col gap-5"
>
<h3
id="carousel-08-title"
className="text-center text-lg font-semibold text-balance"
>
Teams that switched this year
</h3>
<Carousel
setApi={setApi}
opts={{ loop: true }}
aria-labelledby="carousel-08-title"
>
<CarouselContent>
{testimonials.map((item, index) => {
const active = index === current;
return (
<CarouselItem
key={item.company}
aria-label={`${index + 1} of ${testimonials.length}: ${item.company}`}
className="basis-[85%] sm:basis-2/3"
>
<figure
className={cn(
"flex h-full flex-col justify-between gap-6 rounded-xl border bg-card p-5 text-card-foreground transition-[opacity,scale] duration-300 ease-out motion-reduce:transition-none",
active ? "opacity-100" : "scale-95 opacity-40",
)}
>
<blockquote className="text-base leading-relaxed text-pretty">
“{item.quote}”
</blockquote>
<figcaption className="flex items-center gap-3">
<Avatar>
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{item.initials}</AvatarFallback>
</Avatar>
<span className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">
{item.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{item.role}, {item.company}
</span>
</span>
</figcaption>
</figure>
</CarouselItem>
);
})}
</CarouselContent>
</Carousel>
<nav
aria-label="Choose a customer story"
className="flex flex-wrap justify-center gap-x-1 gap-y-1"
>
{testimonials.map((item, index) => (
<button
key={item.company}
type="button"
aria-current={index === current ? "true" : undefined}
onClick={() => api?.scrollTo(index)}
className="rounded-md px-2.5 py-1.5 text-sm font-semibold text-muted-foreground transition-colors outline-none decoration-2 underline-offset-8 hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 aria-current:text-foreground aria-current:underline"
>
{item.company}
</button>
))}
</nav>
</section>
);
}
npx shadcn@latest add @sevenui/component/carousel-08pnpm dlx shadcn@latest add @sevenui/component/carousel-08yarn dlx shadcn@latest add @sevenui/component/carousel-08bunx --bun shadcn@latest add @sevenui/component/carousel-08Triage in one inbox
New issues from GitHub, Slack, and support tickets land in a single queue sorted by urgency.
Link pull requests
Mention an issue ID in a branch name and its status moves to In review the moment the PR opens.
Stay on the keyboard
Press C to create an issue from anywhere, or open the command menu to jump between projects.
Try it now:⌘K
"use client";
import * as React from "react";
import { cn } from "cn";
import {
CheckIcon,
GitPullRequestIcon,
InboxIcon,
KeyboardIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
const steps = [
{
id: "triage",
icon: InboxIcon,
title: "Triage in one inbox",
body: "New issues from GitHub, Slack, and support tickets land in a single queue sorted by urgency.",
},
{
id: "review",
icon: GitPullRequestIcon,
title: "Link pull requests",
body: "Mention an issue ID in a branch name and its status moves to In review the moment the PR opens.",
},
{
id: "shortcuts",
icon: KeyboardIcon,
title: "Stay on the keyboard",
body: "Press C to create an issue from anywhere, or open the command menu to jump between projects.",
shortcut: ["⌘", "K"],
},
];
export default function Carousel09() {
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
const [finished, setFinished] = React.useState(false);
React.useEffect(() => {
if (!api) return;
const onSelect = () => setCurrent(api.selectedScrollSnap());
onSelect();
api.on("select", onSelect);
return () => {
api.off("select", onSelect);
};
}, [api]);
const isLast = current === steps.length - 1;
if (finished) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-3 rounded-xl border bg-card p-8 text-center text-card-foreground">
<span className="flex size-10 items-center justify-center rounded-full bg-primary text-primary-foreground">
<CheckIcon aria-hidden="true" />
</span>
<h3 className="font-medium">You're all set</h3>
<p className="text-sm text-muted-foreground">
Your workspace is ready. You can replay this tour from Help at any
time.
</p>
<Button
variant="outline"
onClick={() => {
setFinished(false);
setCurrent(0);
}}
>
Replay tour
</Button>
</div>
);
}
return (
<div className="flex w-full max-w-sm flex-col overflow-hidden rounded-xl border bg-card text-card-foreground">
<Carousel setApi={setApi} aria-label="Getting started tour">
<CarouselContent>
{steps.map((step, index) => (
<CarouselItem
key={step.id}
aria-label={`Step ${index + 1} of ${steps.length}`}
>
<div className="flex aspect-[16/9] items-center justify-center bg-muted">
<step.icon
aria-hidden="true"
className="size-10 text-muted-foreground"
strokeWidth={1.5}
/>
</div>
<div className="flex flex-col gap-2 p-6 pb-2">
<h3 className="font-medium">{step.title}</h3>
<p className="text-sm text-muted-foreground">{step.body}</p>
{step.shortcut && (
<p className="flex items-center gap-1 text-xs text-muted-foreground">
Try it now:
<KbdGroup>
{step.shortcut.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
</p>
)}
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="flex items-center justify-between gap-4 p-6 pt-4">
<div className="flex items-center gap-1.5">
{steps.map((step, index) => (
<button
key={step.id}
type="button"
aria-label={`Go to step ${index + 1}`}
aria-current={index === current ? "step" : undefined}
onClick={() => api?.scrollTo(index)}
className={cn(
"h-1.5 rounded-full outline-none transition-all focus-visible:ring-3 focus-visible:ring-ring/50",
index === current
? "w-5 bg-primary"
: "w-1.5 bg-muted-foreground/30 hover:bg-muted-foreground/60",
)}
/>
))}
</div>
<div className="flex items-center gap-2">
{!isLast && (
<Button variant="ghost" onClick={() => setFinished(true)}>
Skip
</Button>
)}
<Button
onClick={() => (isLast ? setFinished(true) : api?.scrollNext())}
>
{isLast ? "Open workspace" : "Continue"}
</Button>
</div>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-09pnpm dlx shadcn@latest add @sevenui/component/carousel-09yarn dlx shadcn@latest add @sevenui/component/carousel-09bunx --bun shadcn@latest add @sevenui/component/carousel-09Change plan
Acme Studio · 5 seats
Pro · $600 billed yearly
"use client";
import * as React from "react";
import { cn } from "cn";
import { CheckIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Billing = "monthly" | "yearly";
const seats = 5;
const plans = [
{
id: "starter",
name: "Starter",
monthly: 0,
yearly: 0,
blurb: "For side projects and trying things out.",
features: ["3 projects", "7-day history", "Community support"],
},
{
id: "pro",
name: "Pro",
monthly: 12,
yearly: 10,
blurb: "For small teams shipping every week.",
features: ["Unlimited projects", "90-day history", "Email support"],
recommended: true,
},
{
id: "business",
name: "Business",
monthly: 24,
yearly: 20,
blurb: "For companies that need controls and audit trails.",
features: ["SAML single sign-on", "Unlimited history", "Audit log export"],
},
];
export default function Carousel10() {
const [billing, setBilling] = React.useState<Billing>("yearly");
const [selectedId, setSelectedId] = React.useState("pro");
const [currentPlanId, setCurrentPlanId] = React.useState("starter");
const [api, setApi] = React.useState<CarouselApi>();
const selected = plans.find((plan) => plan.id === selectedId) ?? plans[1];
const perSeat = selected[billing];
const total = billing === "yearly" ? perSeat * seats * 12 : perSeat * seats;
const isCurrent = selected.id === currentPlanId;
const currentPlan =
plans.find((plan) => plan.id === currentPlanId) ?? plans[0];
return (
<div className="flex w-full max-w-md flex-col gap-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-col">
<h3 className="font-medium" id="carousel-10-title">
Change plan
</h3>
<p className="text-sm text-muted-foreground">
Acme Studio · {seats} seats
</p>
</div>
<ToggleGroup
aria-label="Billing period"
variant="outline"
size="sm"
spacing={0}
value={[billing]}
onValueChange={(next) => {
if (next.length > 0) setBilling(next[0] as Billing);
}}
>
<ToggleGroupItem
value="monthly"
className="aria-pressed:bg-accent aria-pressed:text-accent-foreground"
>
Monthly
</ToggleGroupItem>
<ToggleGroupItem
value="yearly"
className="aria-pressed:bg-accent aria-pressed:text-accent-foreground"
>
Yearly −17%
</ToggleGroupItem>
</ToggleGroup>
</div>
{/* Centered so the plans on either side peek in; clicking one brings
it to the middle. */}
<Carousel
setApi={setApi}
aria-labelledby="carousel-10-title"
opts={{ align: "center", startIndex: 1 }}
>
<CarouselContent className="-ml-3 py-1">
{plans.map((plan, index) => {
const active = plan.id === selectedId;
return (
<CarouselItem
key={plan.id}
className="basis-[82%] pl-3 sm:basis-[62%]"
>
<button
type="button"
aria-pressed={active}
onClick={() => {
setSelectedId(plan.id);
api?.scrollTo(index);
}}
className={cn(
"flex h-full w-full flex-col gap-4 rounded-xl border bg-card p-4 text-left text-card-foreground outline-none transition-[border-color,box-shadow] focus-visible:ring-3 focus-visible:ring-ring/50",
active
? "border-primary ring-1 ring-primary"
: "hover:border-foreground/30",
)}
>
<span className="flex items-center justify-between gap-2">
<span className="font-medium">{plan.name}</span>
{plan.id === currentPlanId ? (
<Badge variant="outline">Current</Badge>
) : plan.recommended ? (
<Badge variant="secondary">Most teams</Badge>
) : null}
</span>
<span className="flex items-baseline gap-1">
<span className="text-2xl font-semibold tabular-nums">
${plan[billing]}
</span>
<span className="text-xs text-muted-foreground">
per seat / month
</span>
</span>
<span className="text-sm text-muted-foreground">
{plan.blurb}
</span>
<span className="flex flex-col gap-1.5 text-sm">
{plan.features.map((feature) => (
<span key={feature} className="flex items-center gap-2">
<CheckIcon
aria-hidden="true"
className="size-3.5 text-muted-foreground"
/>
{feature}
</span>
))}
</span>
</button>
</CarouselItem>
);
})}
</CarouselContent>
</Carousel>
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl bg-muted px-4 py-3">
<p className="text-sm" aria-live="polite">
{isCurrent ? (
<span className="text-muted-foreground">
You're on {selected.name}.
</span>
) : (
<>
<span className="font-medium">{selected.name}</span>
<span className="text-muted-foreground">
{" "}
· ${total.toLocaleString("en-US")} billed{" "}
{billing === "yearly" ? "yearly" : "monthly"}
</span>
</>
)}
</p>
<Button
disabled={isCurrent}
onClick={() => setCurrentPlanId(selected.id)}
>
{isCurrent
? "Current plan"
: selected.monthly < currentPlan.monthly
? `Downgrade to ${selected.name}`
: `Upgrade to ${selected.name}`}
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-10pnpm dlx shadcn@latest add @sevenui/component/carousel-10yarn dlx shadcn@latest add @sevenui/component/carousel-10bunx --bun shadcn@latest add @sevenui/component/carousel-10Book a product demo
30 min · Video call · Times in CET
"use client";
import * as React from "react";
import { cn } from "cn";
import { CalendarCheckIcon, VideoIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const allSlots = [
"9:00 AM",
"9:30 AM",
"10:30 AM",
"11:00 AM",
"1:30 PM",
"2:00 PM",
"3:30 PM",
"4:30 PM",
];
// Two weeks of availability starting Monday, September 28.
const days = Array.from({ length: 14 }, (_, index) => {
const date = new Date(2026, 8, 28 + index);
const weekday = date.getDay();
const weekend = weekday === 0 || weekday === 6;
// Deterministic sample availability: some weekdays are partly booked.
const slots = weekend
? []
: allSlots.filter((_, slot) => (slot + index) % 3 !== 0);
return {
id: date.toISOString().slice(0, 10),
weekday: weekdays[weekday],
day: date.getDate(),
month: date.toLocaleString("en-US", { month: "short" }),
slots,
};
});
export default function Carousel11() {
const [dayId, setDayId] = React.useState(days[1].id);
const [time, setTime] = React.useState<string | null>(null);
const [booked, setBooked] = React.useState(false);
const dayIndex = Math.max(
days.findIndex((item) => item.id === dayId),
0,
);
const day = days[dayIndex];
const label = `${day.weekday}, ${day.month} ${day.day}`;
if (booked && time) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-3 rounded-xl border bg-card p-8 text-center text-card-foreground">
<CalendarCheckIcon aria-hidden="true" className="size-8 text-success" />
<h3 className="font-medium">Demo booked</h3>
<p className="text-sm text-muted-foreground">
{label} at {time} (CET) with Maya Chen. A calendar invite and a
video link are on their way to your inbox.
</p>
<Button variant="outline" onClick={() => setBooked(false)}>
Reschedule
</Button>
</div>
);
}
return (
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-4 text-card-foreground">
<div className="flex flex-col gap-1">
<h3 className="font-medium">Book a product demo</h3>
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<VideoIcon aria-hidden="true" className="size-3.5" />
30 min · Video call · Times in CET
</p>
</div>
<Carousel
aria-label="Choose a day"
// Reopen on the page that holds the selected day (5 days per page).
opts={{
align: "start",
slidesToScroll: 5,
startIndex: Math.floor(dayIndex / 5),
}}
>
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium">September – October</span>
<div className="flex gap-1">
<CarouselPrevious className="static my-0" />
<CarouselNext className="static my-0" />
</div>
</div>
<CarouselContent className="-ml-1.5 py-1">
{days.map((item) => {
const selected = item.id === dayId;
const empty = item.slots.length === 0;
const availability = empty
? "no availability"
: `${item.slots.length} times available`;
return (
<CarouselItem key={item.id} className="basis-1/5 pl-1.5">
<button
type="button"
disabled={empty}
aria-pressed={selected}
aria-label={`${item.weekday}, ${item.month} ${item.day}, ${availability}`}
onClick={() => {
setDayId(item.id);
setTime(null);
}}
className={cn(
"flex w-full flex-col items-center gap-0.5 rounded-lg border py-2 outline-none transition-colors focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-40",
selected
? "border-primary bg-primary text-primary-foreground"
: "hover:bg-muted",
)}
>
<span
className={cn(
"text-[0.6875rem]",
selected
? "text-primary-foreground/80"
: "text-muted-foreground",
)}
>
{item.weekday}
</span>
<span className="text-sm font-medium tabular-nums">
{item.day}
</span>
</button>
</CarouselItem>
);
})}
</CarouselContent>
</Carousel>
<fieldset className="flex flex-col gap-2">
<legend className="mb-2 text-sm font-medium">{label}</legend>
<div className="grid grid-cols-3 gap-2">
{day.slots.map((slot) => (
<Button
key={slot}
variant={slot === time ? "default" : "outline"}
size="sm"
aria-pressed={slot === time}
onClick={() => setTime(slot)}
className="tabular-nums"
>
{slot}
</Button>
))}
</div>
</fieldset>
<Button disabled={!time} onClick={() => setBooked(true)}>
{time ? `Confirm ${time}` : "Pick a time"}
</Button>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-11pnpm dlx shadcn@latest add @sevenui/component/carousel-11yarn dlx shadcn@latest add @sevenui/component/carousel-11bunx --bun shadcn@latest add @sevenui/component/carousel-11Suggested teammates
11 of 15 seats used
Commented on 4 files this week
Mentioned in #web-release
Uses the same @northwind.io domain
Reviewed 2 shared prototypes
"use client";
import * as React from "react";
import { CheckIcon, UserPlusIcon, XIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
const initialPeople = [
{
id: "priya",
name: "Priya Raman",
initials: "PR",
role: "Product Designer",
reason: "Commented on 4 files this week",
},
{
id: "daniel",
name: "Daniel Okafor",
initials: "DO",
role: "Frontend Engineer",
reason: "Mentioned in #web-release",
},
{
id: "lena",
name: "Lena Fischer",
initials: "LF",
role: "Engineering Manager",
reason: "Uses the same @northwind.io domain",
},
{
id: "marco",
name: "Marco Silva",
initials: "MS",
role: "QA Lead",
reason: "Reviewed 2 shared prototypes",
},
];
export default function Carousel12() {
const [people, setPeople] = React.useState(initialPeople);
const [invited, setInvited] = React.useState<string[]>([]);
const [status, setStatus] = React.useState("");
const invite = (id: string, name: string) => {
setInvited((prev) => [...prev, id]);
setStatus(`Invitation sent to ${name}.`);
};
const dismiss = (id: string, name: string) => {
setPeople((prev) => prev.filter((person) => person.id !== id));
setStatus(`${name} removed from suggestions.`);
};
return (
<Carousel
aria-labelledby="carousel-12-title"
opts={{ align: "start" }}
className="flex w-full max-w-md flex-col gap-3"
>
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col">
<h3 id="carousel-12-title" className="font-medium">
Suggested teammates
</h3>
<p className="text-sm text-muted-foreground">
{11 + invited.length} of 15 seats used
</p>
</div>
{people.length > 0 && (
<div className="flex gap-1">
<CarouselPrevious className="static my-0" />
<CarouselNext className="static my-0" />
</div>
)}
</div>
{people.length === 0 ? (
<p className="rounded-xl border border-dashed p-6 text-center text-sm text-muted-foreground">
No more suggestions. Invite people by email from Members.
</p>
) : (
<CarouselContent className="-ml-3">
{people.map((person) => {
const isInvited = invited.includes(person.id);
return (
<CarouselItem
key={person.id}
aria-label={person.name}
className="basis-[72%] pl-3 sm:basis-1/2"
>
<div className="relative flex h-full flex-col items-center gap-3 rounded-xl border bg-card p-4 text-center text-card-foreground">
<Button
variant="ghost"
size="icon-xs"
aria-label={`Dismiss ${person.name}`}
onClick={() => dismiss(person.id, person.name)}
className="absolute top-2 right-2 text-muted-foreground"
>
<XIcon aria-hidden="true" />
</Button>
<Avatar size="lg" className="mt-2">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{person.initials}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{person.name}</span>
<span className="text-xs text-muted-foreground">
{person.role}
</span>
</div>
<p className="text-xs text-muted-foreground">
{person.reason}
</p>
<Button
size="sm"
variant={isInvited ? "secondary" : "outline"}
disabled={isInvited}
onClick={() => invite(person.id, person.name)}
className="mt-auto w-full"
>
{isInvited ? (
<CheckIcon aria-hidden="true" />
) : (
<UserPlusIcon aria-hidden="true" />
)}
{isInvited ? "Invited" : "Invite"}
</Button>
</div>
</CarouselItem>
);
})}
</CarouselContent>
)}
<p className="sr-only" aria-live="polite">
{status}
</p>
</Carousel>
);
}
npx shadcn@latest add @sevenui/component/carousel-12pnpm dlx shadcn@latest add @sevenui/component/carousel-12yarn dlx shadcn@latest add @sevenui/component/carousel-12bunx --bun shadcn@latest add @sevenui/component/carousel-12Order summary
- Conical burr grinderMatte black$249.00
Add $51.00 more for free shipping.
- Subtotal
- $249.00
- Shipping
- $12.00
- Total
- $261.00
"use client";
import * as React from "react";
import { CheckIcon, PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
const freeShippingAt = 300;
const shippingFee = 12;
const cartItems = [
{
id: "grinder",
name: "Conical burr grinder",
detail: "Matte black",
price: 249,
},
];
const addOns = [
{ id: "scale", name: "Brew scale", detail: "0.1 g precision", price: 39 },
{ id: "filters", name: "Paper filters", detail: "Pack of 100", price: 9 },
{ id: "brush", name: "Cleaning brush", detail: "Boar bristle", price: 14 },
{ id: "beans", name: "House espresso", detail: "Whole bean, 1 kg", price: 32 },
];
const money = (value: number) =>
value.toLocaleString("en-US", { style: "currency", currency: "USD" });
export default function Carousel13() {
const [added, setAdded] = React.useState<string[]>([]);
const toggle = (id: string) =>
setAdded((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
);
const lines = [
...cartItems,
...addOns.filter((addOn) => added.includes(addOn.id)),
];
const subtotal = lines.reduce((sum, line) => sum + line.price, 0);
const remaining = Math.max(freeShippingAt - subtotal, 0);
const shipping = remaining === 0 ? 0 : shippingFee;
return (
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-4 text-card-foreground">
<h3 className="font-medium">Order summary</h3>
<ul className="flex flex-col gap-3">
{lines.map((line) => (
<li key={line.id} className="flex items-center gap-3">
<img
src="/placeholder.svg"
alt=""
className="size-10 shrink-0 rounded-md border bg-muted object-cover"
/>
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate text-sm font-medium">{line.name}</span>
<span className="text-xs text-muted-foreground">
{line.detail}
</span>
</div>
<span className="text-sm tabular-nums">{money(line.price)}</span>
</li>
))}
</ul>
<Carousel
aria-labelledby="carousel-13-title"
opts={{ align: "start" }}
className="flex flex-col gap-2 rounded-lg bg-muted p-3"
>
<div className="flex items-center justify-between">
<span id="carousel-13-title" className="text-sm font-medium">
Pairs well with
</span>
<div className="flex gap-1">
<CarouselPrevious className="static my-0" />
<CarouselNext className="static my-0" />
</div>
</div>
<CarouselContent className="-ml-2">
{addOns.map((addOn) => {
const isAdded = added.includes(addOn.id);
return (
<CarouselItem
key={addOn.id}
aria-label={addOn.name}
className="basis-[46%] pl-2"
>
<div className="flex h-full flex-col gap-2 rounded-md border bg-background p-2">
<img
src="/placeholder.svg"
alt=""
className="aspect-[4/3] w-full rounded-sm bg-muted object-cover"
/>
<div className="flex flex-col">
<span className="truncate text-xs font-medium">
{addOn.name}
</span>
<span className="text-xs text-muted-foreground tabular-nums">
{money(addOn.price)}
</span>
</div>
<Button
size="xs"
variant={isAdded ? "secondary" : "outline"}
aria-pressed={isAdded}
aria-label={`${isAdded ? "Remove" : "Add"} ${addOn.name}`}
onClick={() => toggle(addOn.id)}
className="mt-auto w-full"
>
{isAdded ? (
<CheckIcon aria-hidden="true" />
) : (
<PlusIcon aria-hidden="true" />
)}
{isAdded ? "Added" : "Add"}
</Button>
</div>
</CarouselItem>
);
})}
</CarouselContent>
</Carousel>
<div className="flex flex-col gap-2">
<p className="text-xs text-muted-foreground" aria-live="polite">
{remaining === 0
? "You've unlocked free shipping."
: `Add ${money(remaining)} more for free shipping.`}
</p>
<Progress
value={Math.min((subtotal / freeShippingAt) * 100, 100)}
aria-label="Progress toward free shipping"
/>
</div>
<Separator />
<dl className="grid grid-cols-[1fr_auto] gap-y-1.5 text-sm">
<dt className="text-muted-foreground">Subtotal</dt>
<dd className="text-right tabular-nums">{money(subtotal)}</dd>
<dt className="text-muted-foreground">Shipping</dt>
<dd className="text-right tabular-nums">
{shipping === 0 ? "Free" : money(shipping)}
</dd>
<dt className="font-medium">Total</dt>
<dd className="text-right font-medium tabular-nums">
{money(subtotal + shipping)}
</dd>
</dl>
<Button size="lg" className="w-full">
Continue to payment
</Button>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-13pnpm dlx shadcn@latest add @sevenui/component/carousel-13yarn dlx shadcn@latest add @sevenui/component/carousel-13bunx --bun shadcn@latest add @sevenui/component/carousel-13WEB-482 · Checkout button hidden on iOS Safari
4 attachments
"use client";
import * as React from "react";
import {
DownloadIcon,
FileTextIcon,
ImageIcon,
PaperclipIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Carousel,
type CarouselApi,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
const attachments = [
{
id: "safari-checkout",
name: "safari-checkout.png",
kind: "image",
size: "412 KB",
uploadedBy: "Nora Blake",
caption: "iPhone 15, Safari 18 — Pay button sits under the home bar.",
},
{
id: "chrome-checkout",
name: "chrome-checkout.png",
kind: "image",
size: "388 KB",
uploadedBy: "Nora Blake",
caption: "Pixel 8, Chrome — same screen renders correctly.",
},
{
id: "safe-area-fix",
name: "safe-area-fix.png",
kind: "image",
size: "295 KB",
uploadedBy: "Omar Haddad",
caption: "Proposed fix: pad the sticky footer with the safe-area inset.",
},
{
id: "qa-report",
name: "qa-report-sep-22.pdf",
kind: "document",
size: "1.2 MB",
uploadedBy: "Omar Haddad",
caption: "Regression checklist for the checkout flow, 18 of 18 passing.",
},
];
export default function Carousel14() {
const [open, setOpen] = React.useState(false);
const [startIndex, setStartIndex] = React.useState(0);
const [api, setApi] = React.useState<CarouselApi>();
const [current, setCurrent] = React.useState(0);
React.useEffect(() => {
if (!api) return;
const onSelect = () => setCurrent(api.selectedScrollSnap());
onSelect();
api.on("select", onSelect);
return () => {
api.off("select", onSelect);
};
}, [api]);
const openAt = (index: number) => {
setStartIndex(index);
setCurrent(index);
setOpen(true);
};
const file = attachments[current];
return (
<div className="flex w-full max-w-md flex-col gap-3 rounded-xl border bg-card p-4 text-card-foreground">
<div className="flex flex-col gap-1">
<h3 className="text-sm font-medium">
WEB-482 · Checkout button hidden on iOS Safari
</h3>
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<PaperclipIcon aria-hidden="true" className="size-3.5" />
{attachments.length} attachments
</p>
</div>
<ul className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{attachments.map((attachment, index) => (
<li key={attachment.id}>
<button
type="button"
onClick={() => openAt(index)}
aria-label={`Preview ${attachment.name}`}
className="flex w-full flex-col overflow-hidden rounded-lg border bg-background text-left outline-none transition-colors hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50"
>
{attachment.kind === "image" ? (
<img
src="/placeholder.svg"
alt=""
className="aspect-[4/3] w-full bg-muted object-cover"
/>
) : (
<span className="flex aspect-[4/3] w-full items-center justify-center bg-muted">
<FileTextIcon
aria-hidden="true"
className="size-6 text-muted-foreground"
/>
</span>
)}
<span className="truncate px-2 py-1.5 text-xs">
{attachment.name}
</span>
</button>
</li>
))}
</ul>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-xl">
<div className="flex flex-col gap-0.5 border-b p-4 pr-12">
<DialogTitle className="flex items-center gap-2 truncate">
{file.kind === "image" ? (
<ImageIcon aria-hidden="true" className="size-4 shrink-0" />
) : (
<FileTextIcon aria-hidden="true" className="size-4 shrink-0" />
)}
<span className="truncate">{file.name}</span>
</DialogTitle>
<DialogDescription className="tabular-nums">
{current + 1} of {attachments.length} · {file.size} · Uploaded by{" "}
{file.uploadedBy}
</DialogDescription>
</div>
<Carousel
setApi={setApi}
opts={{ startIndex }}
aria-label="Attachment previews"
className="bg-muted"
>
<CarouselContent>
{attachments.map((attachment, index) => (
<CarouselItem
key={attachment.id}
aria-label={`${index + 1} of ${attachments.length}: ${attachment.name}`}
>
{attachment.kind === "image" ? (
<img
src="/placeholder.svg"
alt={attachment.caption}
className="aspect-video w-full object-contain"
/>
) : (
<div className="flex aspect-video w-full flex-col items-center justify-center gap-2 text-muted-foreground">
<FileTextIcon
aria-hidden="true"
className="size-10"
strokeWidth={1.5}
/>
<span className="text-sm">PDF preview unavailable</span>
</div>
)}
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-3 bg-background/90" />
<CarouselNext className="right-3 bg-background/90" />
</Carousel>
<div className="flex flex-col gap-3 p-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-muted-foreground">{file.caption}</p>
<Button
variant="outline"
className="shrink-0"
nativeButton={false}
render={<a href="/placeholder.svg" download={file.name} />}
>
<DownloadIcon aria-hidden="true" />
Download
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}
npx shadcn@latest add @sevenui/component/carousel-14pnpm dlx shadcn@latest add @sevenui/component/carousel-14yarn dlx shadcn@latest add @sevenui/component/carousel-14bunx --bun shadcn@latest add @sevenui/component/carousel-14