Customization
Compose parts, restyle anything with className, pick variants, and edit the source.
Compose parts
Most components are compound: a root plus named parts. Use the parts you need, in the order you want, and put anything else in between.
<ProjectCard href="/work/lumen" variant="minimal">
<ProjectCardMedia>
<img src="/lumen.jpg" alt="" />
</ProjectCardMedia>
<ProjectCardContent>
<ProjectCardMeta>
<span>2025</span>
<span>Design lead</span>
</ProjectCardMeta>
<ProjectCardTitle>Lumen</ProjectCardTitle>
<ProjectCardDescription>A design system for eleven products.</ProjectCardDescription>
</ProjectCardContent>
</ProjectCard>List-like components also take data (tags={[…]}, items={[…]}) for the common case. When you need more control, render the parts yourself.
Restyle with className
Every part accepts className. It's merged last with cn(), so your classes win:
<ProjectCardTitle className="text-3xl">Lumen</ProjectCardTitle>Every part also forwards the rest of its props (id, aria-*, event handlers, style) to its DOM element.
Variants
Components with more than one visual style take a variant (and sometimes size) prop. The class recipes are exported too, e.g. projectCardVariants, so you can reuse them on your own elements.
Target parts with data-slot
Each part renders a data-slot attribute (data-slot="project-card-title"). Style a part from a parent without touching the component:
<div className="[&_[data-slot=project-card-title]]:text-highlight-ink">…</div>Links and routers
Components render plain <a> elements. To use your router's link, wrap the component or pass onNavigate where offered (CommandMenu). Components that accept asChild (like ArrowLink) render your element instead:
<ArrowLink asChild>
<Link href="/about">About me</Link>
</ArrowLink>Motion
Entrance and pointer effects respect prefers-reduced-motion. Durations and easing use the kit's ease-smooth curve. To tone motion down globally, remove the effect components you don't want, or pass animate={false} where offered.
Pages print cleanly without extra work. Entrance effects (Reveal, TextReveal, Terminal, NumberTicker, ScrambleText) print their final state even if they never scrolled into view, and the Preloader is hidden. Add Tailwind's print: variants for anything else, e.g. print:hidden on a header.
Remembered state on server-rendered pages
A few components remember a visitor's choice in browser storage. The server can't read it, so on server-rendered pages (Next.js) the first paint shows the default until the page hydrates. Each one exports an optional inline script that applies the stored choice before that first paint. Add the ones you use to <head> in your root layout:
import { createPreloaderScript } from "@/components/portfolio/preloader"
// inside <head>, with the same key you pass to <Preloader once storageKey="…" />
<script dangerouslySetInnerHTML={{ __html: createPreloaderScript("creative-intro") }} />If storage is blocked, the scripts do nothing and the component shows its default.
Match the kit in your own components
When you write your own sections, reuse the same conventions so they sit naturally next to kit components:
- Type: the recipes in
@/lib/typography(typography.title,typography.eyebrow,typography.meta…). Small labels are the monoeyebrowstyle. - Cards:
rounded-2xl border bg-card shadow-cardwithp-5 md:p-6; linked cards lift by-translate-y-1on hover. - Pills:
rounded-pill,h-8for labels,size-9for icon buttons,h-11for main actions. Arrow chips aresize-7with asize-3.5arrow. - Rows: divided with
border-b,py-5 md:py-6. - Links in text: underline at
decoration-foreground/30, turningdecoration-highlight-inkon hover. - Motion:
ease-smooth, 200–300ms for hover, 500–800ms for entrances, always behindmotion-safe:or with amotion-reduce:fallback.
Edit the source
It's your code. Change the markup, add props, delete parts. Later shadcn add runs will ask before overwriting a file you changed.