Landing page: classic
A centered hero with a command to copy, a wall of live components, six features, three install steps, a list of examples, questions and a closing call to action. Its own header and footer are optional.
Add this example to a project that has prfct installed. The command writes the files below and installs the components they use.
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/landing.json"use client"
import * as React from "react"
import Link from "next/link"
import {
AccessibilityIcon,
ArrowRightIcon,
CheckIcon,
CodeIcon,
ContrastIcon,
CopyIcon,
LanguagesIcon,
LayoutTemplateIcon,
PaletteIcon,
} from "lucide-react"
import Showcase from "./landing-showcase"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Badge } from "@/components/ui/badge"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
const install = "pnpm dlx shadcn@latest add https://prfct.dev/r/prfct.json"
// Links that start with "/" are prfct.dev's own pages: point them at yours.
const navigation = [
{ label: "Features", href: "#features" },
{ label: "Examples", href: "#examples" },
{ label: "Questions", href: "#questions" },
]
const features = [
{
icon: ContrastIcon,
title: "Contrast you don't have to check",
text: "Every scale is generated, and its text steps are tested against WCAG AA on the fills they sit on, in light and dark.",
},
{
icon: AccessibilityIcon,
title: "Accessible from the first render",
text: "Keyboard support, focus management and ARIA come with every component, and automated tests check them in both themes.",
},
{
icon: CodeIcon,
title: "Code you own",
text: "The shadcn CLI copies components into your project. Read them, change them, delete what you don't need.",
},
{
icon: PaletteIcon,
title: "One theme, any brand",
text: "Pick a color in the theme builder and paste one stylesheet. Every component follows it.",
},
{
icon: LayoutTemplateIcon,
title: "Whole screens, not only parts",
text: "A dashboard, a checkout, a mail client and more: example apps you install with one command and adapt.",
},
{
icon: LanguagesIcon,
title: "Ready for more than English",
text: "The text built into components, dates and numbers follow the language of your page.",
},
]
const steps = [
{
title: "Start a project",
text: "Create a Next.js app with shadcn. In an app you already have, leave out --template next.",
command:
"pnpm dlx shadcn@latest init --template next --base base --preset nova",
},
{
title: "Add the theme",
text: "Tokens, base styles and font faces arrive as files in your project. The guide shows the stylesheet import and the two font files to download.",
command: install,
},
{
title: "Add what you need",
text: "A single component or a whole example app. Each one brings what it depends on.",
command: "pnpm dlx shadcn@latest add https://prfct.dev/r/dashboard.json",
},
]
const examples = [
{
name: "Dashboard",
text: "Navigation, stats, a traffic chart and a deployments table.",
href: "/examples/dashboard",
},
{
name: "Mail",
text: "Folders, a message list and a reading pane with a reply box.",
href: "/examples/mail",
},
{
name: "Checkout",
text: "Contact, delivery and payment with an order summary in view.",
href: "/examples/checkout",
},
{
name: "Booking",
text: "A calendar, free time slots, a short form and a confirmation.",
href: "/examples/booking",
},
{
name: "Team",
text: "Members, roles that change in place and an invite dialog.",
href: "/examples/team",
},
{
name: "Pricing",
text: "Three plans, a comparison table and answers to questions.",
href: "/examples/pricing",
},
]
const questions = [
{
question: "Is prfct free?",
answer:
"Yes. It is open source under the MIT license, for personal and commercial work.",
},
{
question: "How is it different from shadcn?",
answer:
"prfct is built on shadcn's components and keeps their API. It adds generated tokens with tested contrast, a quieter look, documentation for every component and example apps.",
},
{
question: "Do I need Next.js?",
answer:
"prfct is built and tested for Next.js. The components are React and Tailwind CSS, so other React frameworks may work, but nobody has tested them.",
},
{
question: "Can I use my own brand color?",
answer:
"Yes. The theme builder regenerates every scale from one color and keeps text readable in both modes.",
},
{
question: "How do I update a component?",
answer:
"Run the add command again. The CLI asks before it replaces a file, so you can look at the change first.",
},
]
const footer = [
{
title: "Product",
links: [
{ label: "Components", href: "/docs/components" },
{ label: "Foundations", href: "/docs/foundations" },
{ label: "Themes", href: "/themes" },
{ label: "Examples", href: "/examples" },
],
},
{
title: "Resources",
links: [
{ label: "Documentation", href: "/docs" },
{ label: "Installation", href: "/docs/installation" },
{ label: "Changelog", href: "/docs/resources/changelog" },
],
},
]
/** One width and one gutter for every section. */
const container = "mx-auto w-full max-w-6xl px-4 @md:px-6"
function Logo() {
return (
<Link href="/" className="flex items-center gap-2 rounded-md focus-ring">
<svg viewBox="0 0 24 24" fill="currentColor" className="size-6">
<path d="M3 21V3a18 18 0 0 1 18 18z" />
</svg>
<span className="font-semibold tracking-tight">prfct</span>
</Link>
)
}
/** A command people copy rather than retype. */
function Command({ children }: { children: string }) {
const [copied, setCopied] = React.useState(false)
// The check mark confirms the copy for a moment, then turns back.
React.useEffect(() => {
if (!copied) return
const timer = setTimeout(() => setCopied(false), 2000)
return () => clearTimeout(timer)
}, [copied])
async function copy() {
try {
await navigator.clipboard.writeText(children)
setCopied(true)
} catch {
// No clipboard access (an insecure page, a denied permission): nothing
// was copied, so nothing is confirmed.
}
}
return (
<div className="flex max-w-full items-center gap-1 rounded-xl border bg-surface py-1 pr-1 pl-4">
<code className="truncate font-mono text-sm">
<span className="text-muted-foreground select-none">$ </span>
{children}
</code>
<Button
variant="ghost"
size="icon-sm"
aria-label={copied ? "Copied" : "Copy command"}
onClick={copy}
>
{copied ? <CheckIcon /> : <CopyIcon />}
</Button>
</div>
)
}
function SectionHeading({ title, text }: { title: string; text: string }) {
return (
<div className="flex max-w-2xl flex-col gap-3">
<h2 className="text-heading-xl text-balance @2xl:text-display-sm">
{title}
</h2>
<p className="text-body-lg text-pretty text-muted-foreground">{text}</p>
</div>
)
}
function Header() {
return (
<header className="border-b">
<div className={cn(container, "flex h-14 items-center gap-8")}>
<Logo />
<nav aria-label="Sections" className="flex gap-6 @max-2xl:hidden">
{navigation.map((item) => (
<a
key={item.href}
href={item.href}
className="rounded-md text-sm text-muted-foreground focus-ring transition-colors duration-fast hover:text-foreground"
>
{item.label}
</a>
))}
</nav>
<Link
href="/docs/installation"
className={buttonVariants({ size: "sm", className: "ml-auto" })}
>
Get started
</Link>
</div>
</header>
)
}
function Hero() {
return (
<section className={cn(container, "pt-14 pb-12 @2xl:pt-24 @2xl:pb-16")}>
<div className="mx-auto flex max-w-3xl flex-col items-center gap-6 text-center">
<Link
href="/examples"
className="flex items-center gap-2 rounded-full border bg-card py-1 pr-3 pl-1 text-sm focus-ring transition-colors duration-fast hover:border-border-strong"
>
<Badge>New</Badge>
Example apps you can install
<ArrowRightIcon className="size-3.5" aria-hidden="true" />
</Link>
<h1 className="text-display-sm text-balance @2xl:text-display-lg @5xl:text-display-xl">
A design system for Next.js, built on shadcn
</h1>
<p className="max-w-2xl text-body-lg text-pretty text-muted-foreground">
Tokens with tested contrast, accessible components and complete
example apps. You install them with the shadcn CLI, and the code is
yours.
</p>
<div className="flex flex-wrap justify-center gap-3">
<Link
href="/docs/installation"
className={buttonVariants({ size: "lg" })}
>
Get started
<ArrowRightIcon data-icon="inline-end" />
</Link>
<Link
href="/docs/components"
className={buttonVariants({ variant: "outline", size: "lg" })}
>
Browse components
</Link>
</div>
<Command>{install}</Command>
</div>
</section>
)
}
function Components() {
return (
<section aria-label="Components" className={cn(container, "pb-20")}>
{/* A picture of the system, not a form to fill in: nothing in it takes focus. */}
<div inert className="rounded-2xl border bg-surface p-3 @2xl:p-4">
<Showcase />
</div>
<p className="mt-4 text-center text-sm text-muted-foreground">
Every element above is a prfct component in its default theme.
</p>
</section>
)
}
function Features() {
return (
<section id="features" className="border-t">
<div className={cn(container, "py-20 @2xl:py-28")}>
<SectionHeading
title="What you start with"
text="The decisions a design system has to make, already made and written down."
/>
<div className="mt-12 grid gap-x-10 gap-y-12 @2xl:grid-cols-2 @5xl:grid-cols-3">
{features.map(({ icon: Icon, title, text }) => (
<div key={title} className="flex flex-col gap-3">
<span className="flex size-10 items-center justify-center rounded-lg border bg-card">
<Icon className="size-5" aria-hidden="true" />
</span>
<h3 className="text-heading-sm">{title}</h3>
<p className="text-sm leading-6 text-muted-foreground">{text}</p>
</div>
))}
</div>
</div>
</section>
)
}
function Steps() {
return (
<section className="border-t bg-surface">
<div className={cn(container, "py-20 @2xl:py-28")}>
<SectionHeading
title="From an empty folder to a first screen"
text="No package to configure and nothing to wrap your app in."
/>
<ol className="mt-12 grid gap-10 @5xl:grid-cols-3">
{steps.map((step, index) => (
<li key={step.title} className="flex min-w-0 flex-col gap-3">
<span className="font-mono text-sm text-muted-foreground">
0{index + 1}
</span>
<h3 className="text-heading-sm">{step.title}</h3>
<p className="text-sm leading-6 text-muted-foreground">
{step.text}
</p>
<Command>{step.command}</Command>
</li>
))}
</ol>
</div>
</section>
)
}
function Examples() {
return (
<section id="examples" className="border-t">
<div className={cn(container, "py-20 @2xl:py-28")}>
<div className="flex flex-wrap items-end justify-between gap-6">
<SectionHeading
title="Start from a finished screen"
text="Example apps written to be read, each installed with one command."
/>
<Link
href="/examples"
className={buttonVariants({ variant: "outline" })}
>
All examples
<ArrowRightIcon data-icon="inline-end" />
</Link>
</div>
<ul className="mt-12 grid gap-x-10 border-t @2xl:grid-cols-2">
{examples.map((example) => (
<li key={example.href} className="border-b">
<Link
href={example.href}
className="group flex items-center gap-4 rounded-md py-5 focus-ring"
>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="font-medium">{example.name}</span>
<span className="text-sm text-muted-foreground">
{example.text}
</span>
</span>
<ArrowRightIcon
className="size-4 text-muted-foreground transition-transform duration-fast group-hover:translate-x-0.5 group-hover:text-foreground"
aria-hidden="true"
/>
</Link>
</li>
))}
</ul>
</div>
</section>
)
}
function Questions() {
return (
<section id="questions" className="border-t">
<div
className={cn(
container,
"grid gap-10 py-20 @2xl:py-28 @5xl:grid-cols-[2fr_3fr]"
)}
>
<SectionHeading
title="Questions"
text="The ones people ask before they install anything."
/>
<Accordion defaultValue={[questions[0].question]}>
{questions.map((item) => (
<AccordionItem key={item.question} value={item.question}>
<AccordionTrigger>{item.question}</AccordionTrigger>
<AccordionContent>{item.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</section>
)
}
/** Dark in both themes, so the page ends on its one loud moment. */
function Closing() {
return (
<section className="dark border-t bg-background text-foreground">
<div
className={cn(
container,
"flex flex-col items-center gap-6 py-20 text-center @2xl:py-28"
)}
>
<h2 className="max-w-2xl text-heading-xl text-balance @2xl:text-display-md">
Install the theme and build your first screen
</h2>
<Command>{install}</Command>
<div className="flex flex-wrap justify-center gap-3">
<Link
href="/docs/installation"
className={buttonVariants({ size: "lg" })}
>
Read the installation guide
</Link>
<Link
href="/examples"
className={buttonVariants({ variant: "outline", size: "lg" })}
>
See the examples
</Link>
</div>
</div>
</section>
)
}
function Footer() {
return (
<footer className="border-t">
<div
className={cn(
container,
"grid gap-10 py-12 @2xl:grid-cols-[2fr_1fr_1fr]"
)}
>
<div className="flex flex-col items-start gap-3">
<Logo />
<p className="max-w-xs text-sm text-muted-foreground">
A design system for Next.js, built on shadcn. Released under the MIT
license.
</p>
</div>
{footer.map((group) => (
<nav
key={group.title}
aria-label={group.title}
className="flex flex-col gap-3"
>
<h2 className="text-sm font-medium">{group.title}</h2>
<ul className="flex flex-col gap-2">
{group.links.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="rounded-md text-sm text-muted-foreground focus-ring transition-colors duration-fast hover:text-foreground"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
))}
</div>
</footer>
)
}
/**
* A landing page, top to bottom. `chrome` is its own header and footer:
* leave them out where the page sits inside a site that has both, as it
* does on prfct.dev.
*/
export default function LandingExample({
chrome = true,
}: {
chrome?: boolean
}) {
return (
<div className="@container bg-background text-foreground">
{chrome && <Header />}
<Hero />
<Components />
<Features />
<Steps />
<Examples />
<Questions />
<Closing />
{chrome && <Footer />}
</div>
)
}import {
CircleCheckIcon,
GitBranchIcon,
MinusIcon,
PlusIcon,
} from "lucide-react"
import aurora from "./aurora.svg"
import ember from "./ember.svg"
import tide from "./tide.svg"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldSeparator,
FieldTitle,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp"
import { Progress } from "@/components/ui/progress"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Slider } from "@/components/ui/slider"
import { Switch } from "@/components/ui/switch"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
const people = [
{ name: "Maya Chen", role: "Owner", image: aurora.src, owner: true },
{ name: "Jonah Okafor", role: "Admin", image: ember.src },
{ name: "Lena Park", role: "Member", image: tide.src },
]
const plans = [
{ value: "hobby", title: "Hobby", description: "One project", price: "$0" },
{
value: "pro",
title: "Pro",
description: "Unlimited projects",
price: "$20",
},
]
const alerts = [
{ id: "deploys", title: "Deployments", on: true },
{ id: "comments", title: "Comments", on: true },
{ id: "digest", title: "Weekly digest", on: false },
]
function AccountCard() {
return (
<Card size="sm">
<CardHeader>
<CardTitle>Create an account</CardTitle>
<CardDescription>Start with your work email.</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup>
<Field>
<FieldLabel htmlFor="showcase-name">Name</FieldLabel>
<Input id="showcase-name" defaultValue="Ada Lovelace" />
</Field>
<Field>
<FieldLabel htmlFor="showcase-email">Email</FieldLabel>
<Input id="showcase-email" placeholder="ada@example.com" />
</Field>
<Field orientation="horizontal">
<Checkbox id="showcase-updates" defaultChecked />
<FieldLabel htmlFor="showcase-updates" className="font-normal">
Send me product updates
</FieldLabel>
</Field>
<Button>Create account</Button>
<FieldSeparator>or</FieldSeparator>
<Button variant="outline">
<GitBranchIcon data-icon="inline-start" />
Continue with Git
</Button>
</FieldGroup>
</CardContent>
</Card>
)
}
export function TeamCard() {
return (
<Card size="sm">
<CardHeader>
<CardTitle>Team</CardTitle>
<CardDescription>Three people can open this project.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-3">
{people.map((person) => (
<div key={person.name} className="flex items-center gap-3">
<Avatar>
<AvatarImage src={person.image} alt="" />
<AvatarFallback>{person.name[0]}</AvatarFallback>
</Avatar>
<span className="flex-1 truncate text-sm font-medium">
{person.name}
</span>
<Badge variant={person.owner ? "default" : "outline"}>
{person.role}
</Badge>
</div>
))}
</CardContent>
</Card>
)
}
export function PlanCard() {
return (
<Card size="sm">
<CardHeader>
<CardTitle>Plan</CardTitle>
<CardDescription>Change it whenever you like.</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup defaultValue="pro" aria-label="Plan">
{plans.map((plan) => (
<FieldLabel key={plan.value} htmlFor={`showcase-${plan.value}`}>
<Field orientation="horizontal">
<RadioGroupItem
value={plan.value}
id={`showcase-${plan.value}`}
/>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<span className="text-sm font-semibold tabular-nums">
{plan.price}
</span>
</Field>
</FieldLabel>
))}
</RadioGroup>
</CardContent>
</Card>
)
}
export function NotificationsCard() {
return (
<Card size="sm">
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>Changes save as you switch.</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="gap-3">
{alerts.map((alert) => (
<Field key={alert.id} orientation="horizontal">
<FieldLabel
htmlFor={`showcase-${alert.id}`}
className="flex-1 font-normal"
>
{alert.title}
</FieldLabel>
<Switch id={`showcase-${alert.id}`} defaultChecked={alert.on} />
</Field>
))}
</FieldGroup>
</CardContent>
</Card>
)
}
function UsageCard() {
return (
<Card size="sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
Usage <Badge variant="success">On track</Badge>
</CardTitle>
<CardDescription>Resets on the first of the month.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
<div className="flex justify-between text-sm">
<span>Build minutes</span>
<span className="text-muted-foreground tabular-nums">
3,120 of 6,000
</span>
</div>
<Progress value={52} aria-label="Build minutes used" />
</CardContent>
<CardFooter className="justify-end">
<Button variant="outline" size="sm">
Manage plan
</Button>
</CardFooter>
</Card>
)
}
function AppearanceCard() {
return (
<Card size="sm">
<CardHeader>
<CardTitle>Appearance</CardTitle>
<CardDescription>How the editor looks on this device.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-5">
{/* One choice of three with nothing to switch between: a toggle
group, not tabs. */}
<ToggleGroup
variant="outline"
spacing={0}
defaultValue={["system"]}
aria-label="Theme"
className="w-full"
>
<ToggleGroupItem value="light" className="flex-1">
Light
</ToggleGroupItem>
<ToggleGroupItem value="dark" className="flex-1">
Dark
</ToggleGroupItem>
<ToggleGroupItem value="system" className="flex-1">
System
</ToggleGroupItem>
</ToggleGroup>
<div className="flex items-center gap-3 text-muted-foreground">
<MinusIcon className="size-4 shrink-0" aria-hidden="true" />
<Slider defaultValue={60} aria-label="Text size" />
<PlusIcon className="size-4 shrink-0" aria-hidden="true" />
</div>
</CardContent>
</Card>
)
}
function VerifyCard() {
return (
<Card size="sm">
<CardHeader>
<CardTitle>Two-step verification</CardTitle>
<CardDescription>
Enter the code from your authenticator.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-start gap-4">
<InputOTP maxLength={6} aria-label="Verification code">
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<Button size="sm">Verify</Button>
</CardContent>
</Card>
)
}
const column = "flex flex-col gap-4"
/**
* A wall of components in their default theme: what the system looks like
* before anyone has styled anything. One column on a phone, three on a desk.
*/
export default function LandingShowcase() {
return (
<div className="grid gap-4 @2xl:grid-cols-2 @5xl:grid-cols-3">
<div className={column}>
<AccountCard />
<UsageCard />
</div>
<div className={`${column} @max-2xl:hidden`}>
<TeamCard />
<PlanCard />
<Alert variant="success">
<CircleCheckIcon />
<AlertTitle>Deployment ready</AlertTitle>
<AlertDescription>
Production now serves the build from main.
</AlertDescription>
</Alert>
</div>
<div className={`${column} @max-5xl:hidden`}>
<NotificationsCard />
<AppearanceCard />
<VerifyCard />
</div>
</div>
)
}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
<rect width="80" height="80" fill="#e4eaff"/>
<circle cx="40" cy="31" r="14" fill="#8ea6ff"/>
<path d="M12 80c0-15.5 12.5-26 28-26s28 10.5 28 26z" fill="#8ea6ff"/>
</svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
<rect width="80" height="80" fill="#fdebe0"/>
<circle cx="40" cy="31" r="14" fill="#f0a072"/>
<path d="M12 80c0-15.5 12.5-26 28-26s28 10.5 28 26z" fill="#f0a072"/>
</svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
<rect width="80" height="80" fill="#dcf2ef"/>
<circle cx="40" cy="31" r="14" fill="#6cbdb3"/>
<path d="M12 80c0-15.5 12.5-26 28-26s28 10.5 28 26z" fill="#6cbdb3"/>
</svg>import type { Metadata } from "next"
import Landing from "@/components/landing/landing"
export const metadata: Metadata = {
title: "Landing page: classic",
description: "A centered hero with a wall of components, features with icons, install steps, a list of examples and questions.",
}
export default function LandingPage() {
return <Landing />
}