Skip to content

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>

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.

Print

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") }} />
ComponentScriptWithout it
ThemeTogglecolorModeScript (or createColorModeScript(storageKey); also add suppressHydrationWarning to <html>)The wrong color mode flashes
Preloader with oncecreatePreloaderScript(storageKey)The curtain flashes for visitors who already saw the intro
AnnouncementBar with storageKeycreateAnnouncementBarScript(storageKey)A dismissed bar flashes
ContactBar with storageKeycreateContactBarScript(storageKey)A dismissed bar flashes

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 mono eyebrow style.
  • Cards: rounded-2xl border bg-card shadow-card with p-5 md:p-6; linked cards lift by -translate-y-1 on hover.
  • Pills: rounded-pill, h-8 for labels, size-9 for icon buttons, h-11 for main actions. Arrow chips are size-7 with a size-3.5 arrow.
  • Rows: divided with border-b, py-5 md:py-6.
  • Links in text: underline at decoration-foreground/30, turning decoration-highlight-ink on hover.
  • Motion: ease-smooth, 200–300ms for hover, 500–800ms for entrances, always behind motion-safe: or with a motion-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.