Component Architecture
Components are organized in four atomic-design layers under components/. Route pages
almost always compose from the top layer (organisms) inside <SiteShell> — there is
rarely a reason to write markup directly in a page.tsx.
atoms/ primitives
↓
molecules/ small compositions of atoms/
↓
organisms/ full page sections
↓
templates/ the per-page wrapper (SiteShell)atoms/ — primitives
Low-level, style-focused building blocks with no page-specific meaning:
button, tag, stat, eyebrow, accordion, animated-link-label,
brand-mark, corner-cta, history-back-button, icon-feature-card,
logo-strip, parallax-cover-image.
molecules/ — small compositions
Built from atoms/ primitives, still content-agnostic (no fetching, no page-specific
logic): content-card, pricing-card, quote-card, team-member-card,
stat-highlight-card, avatar-identity, section-heading, split-intro, step-grid,
numbered-step, numbered-list-item, tag-list, toc-card, detail-sidebar,
accordion-list, media-content-card, info-card, field.
organisms/ — full page sections
Everything a route composes to build a page: hero variants (hero-section through
hero-section-7), stats-section, pricing-adjacent sections, article-grid-section
(and -2/-3/-4 variants), carousel-section, cta-banner, cta-cards,
accordion-section, feature-list-section, features-section,
bento-highlight-section, comparison-table-section, gallery,
gradient-image-gallery-section, logo-strip-section, newsletter-section,
people-grid-section, pinned-showcase-section, quote-section-1/2/3,
stat-bar-section, stat-results-section, steps-section/steps-section-2,
ticker-section, category-tabs, fact-grid-section, featured-item-section,
icon-feature-grid-section, media-feature-grid-section, numbered-list-section,
form-panel-section, plus the always-present header and footer, and
article-specific organisms (article-card, article-detail-layout,
author-share-section).
There are multiple numbered variants of some sections (e.g. seven hero sections). Before building a new section from scratch, check whether an existing numbered variant already does what you need — the multiplicity exists specifically so you can pick a layout without writing new code.
Forms live in organisms/forms/.
templates/site-shell.tsx — the per-page wrapper
Every route page renders its content inside <SiteShell>:
export function SiteShell({
children,
footerVariant,
}: {
children: ReactNode;
footerVariant: FooterVariant;
}) {
return (
<SiteMotionProvider>
<Header showcase={footerVariant === "minimal"} />
<SiteAnimations>
<div aria-hidden="true" className="h-21 sm:h-23.5" />
<main data-main-container="">
{children}
<Footer variant={footerVariant} />
</main>
</SiteAnimations>
</SiteMotionProvider>
);
}It wires up the GSAP motion context (SiteMotionProvider), the scroll-animation
wrapper (SiteAnimations — see Animation System), the header and the
footer. A route page’s job is just to pick a footerVariant and list which organisms go
in between.
This is also the place to add anything you want pinned above the page on every route. The
corner-cta atom is built for exactly that — a fixed-position button anchored to a bottom
corner — but it is not wired up by default. To use it, render it here with your own link:
<Header showcase={footerVariant === "minimal"} />
<CornerCta href="https://cal.com/you/intro" label="Book a call" />It defaults to the bottom-right; pass position="left" to pin it to the other corner. It
always opens its link in a new tab, so it suits an external destination — a booking page, a
store, a docs site — better than a route on your own site.
footerVariant
SiteShell takes footerVariant: "full" | "minimal" (the FooterVariant type in
components/organisms/footer.tsx), which does two things:
- Selects the footer.
"full"renders the four-column marketing footer — nav columns, contact details, and a newsletter signup."minimal"renders a single-row footer — brand lockup, copyright, and thefooter.showcaseLinkslinks — used only by the template’s own demo page. - Toggles header behavior, via
showcase={footerVariant === "minimal"}passed toHeader. Theshowcaseflag swaps which nav link set the header renders (nav.showcasevs.nav.primary, both fromcontent/site-wide.tsx) and enables smooth-anchor scrolling for on-page section links — it’s how the template’s own demo page gets different nav from real site pages.
For real site pages, use "full". Reserve "minimal" for the template’s own showcase
page (app/page.tsx) — using it elsewhere pulls in the showcase nav links and anchor
behavior.
Restyling a component
Every component owns its own styling. Where something has more than one look — a button
that’s solid on light surfaces and outlined on dark ones, a label that’s muted on white
and translucent on black — those looks are named variants on the component itself,
defined with cva (class-variance-authority):
const eyebrowVariants = cva("font-medium tracking-widest uppercase", {
variants: {
size: { sm: "text-xs", xs: "text-[10px]" },
tone: {
muted: "text-muted-foreground",
foreground: "text-foreground",
inverse: "text-white/50",
},
},
defaultVariants: { size: "sm", tone: "muted" },
});Call sites pick a look by name, never by passing the classes that produce it:
<Eyebrow as="h2" tone="inverse" className="mb-6">
{heading}
</Eyebrow>The prop naming is consistent across the template:
tone— colour (muted,inverse,primary,accent)size— scale, usuallysm/md/lg(a few components use values that suit them better, likeEyebrow’ssm/xsorAvatarIdentity’s32/48)variant— a whole look that is neither colour nor scale alone (Button’sprimary/outline/link,Field’sunderline/pill)
A className on one of these components is for positioning only — margin, grid
placement, alignment. Anything that changes how the component itself looks (colour, type
scale, padding, border, background) belongs in a variant.
That distinction is what makes a single edit travel. Restyle tone: "muted" in
eyebrow.tsx and every eyebrow on every page follows. Paste the classes at a call site
instead and that one instance quietly stops tracking the component, so the next theme
change misses it.
Adding a look that doesn’t exist yet
Add a variant rather than a className:
tone: {
muted: "text-muted-foreground",
inverse: "text-white/50",
brand: "text-primary", // new
},If you catch yourself passing the same class string at two different call sites, that string is a variant waiting to be named.
When to add a new component vs. reuse
- Need a page section that’s a close variant of an existing organism (different heading layout, different number of columns)? Check the numbered variants first — there’s a good chance one already fits.
- Need a genuinely new kind of section? Build it in
organisms/, composing frommolecules//atoms/rather than writing raw markup — this keeps spacing, typography, and responsive behavior consistent with the rest of the site. - Need a new small reusable piece (a card shape, a labeled stat)? That belongs in
molecules/oratoms/depending on how composed it is, not inlined into an organism.