Skip to Content
StatixFlow Next Docs
Component Architecture

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>:

components/templates/site-shell.tsx
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:

components/templates/site-shell.tsx
<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 the footer.showcaseLinks links — used only by the template’s own demo page.
  • Toggles header behavior, via showcase={footerVariant === "minimal"} passed to Header. The showcase flag swaps which nav link set the header renders (nav.showcase vs. nav.primary, both from content/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):

components/atoms/eyebrow.tsx
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, usually sm/md/lg (a few components use values that suit them better, like Eyebrow’s sm/xs or AvatarIdentity’s 32/48)
  • variant — a whole look that is neither colour nor scale alone (Button’s primary/outline/link, Field’s underline/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 from molecules//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/ or atoms/ depending on how composed it is, not inlined into an organism.
Last updated on