Component Library Notes - Conventions, Styling Tokens, and Composition

Status: Active
Last Updated: 2026-08-26
Category: Frontend - Architecture
Prerequisites: website-rebuild, tailwind-v4-migration
Time: 1 hour
Tags: react, components, tailwind, composition, design-system, conventions

Summary

Working conventions for React components in the fogserv.cloud website src/components/ tree: how to structure and name components, how Tailwind styling tokens are consumed, when to reach for headless UI primitives vs hand-rolled markup, and why composition beats configuration for a small team.

๐ŸŽฏ What You'll Learn

By the end of this article, you'll be able to:


Context / Why This Matters

The site is small enough that a formal published component library would be overhead โ€” but not so small that ad-hoc components stay consistent. These notes codify what already exists in src/components/ so new pages (routing-patterns) reuse rather than reinvent. Styling rides on the Tailwind v4 token layer described in tailwind-v4-migration; this article covers how components consume it.

Implementation / Core Content

Layout and Naming

src/
โ”œโ”€โ”€ components/     # shared UI: buttons, cards, layout shells
โ”œโ”€โ”€ routes/         # page-specific composition (may have local subcomponents)
โ”œโ”€โ”€ hooks/          # reusable behavior (useXxx)
โ”œโ”€โ”€ services/       # API/data clients used by loaders
โ””โ”€โ”€ types/

Rules:

Styling Token Consumption

Components never hard-code hex values. They use token-derived utility classes:

// Good โ€” token classes from @theme/config
<button className="rounded-md bg-primary px-4 py-2 text-white hover:bg-primary-hover">
  Subscribe
</button>

// Bad โ€” bypasses the design system
<button className="bg-[#2f6f5e]">Subscribe</button>

Available families (see tailwind.config.ts today / @theme after tailwind-v4-migration): primary, primary-hover, primary-light, secondary (+hover/light), accent-tan, accent-terracotta (+variants). Semantic guidance:

Long class strings are normal with Tailwind; extract a component instead of abstracting the string:

// src/components/Button.tsx
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'secondary'
}
export function Button({ variant = 'primary', className, ...rest }: ButtonProps) {
  return (
    <button
      className={`rounded-md px-4 py-2 text-sm font-medium ${VARIANTS[variant]} ${className ?? ''}`}
      {...rest}
    />
  )
}
const VARIANTS = {
  primary: 'bg-primary text-white hover:bg-primary-hover',
  secondary: 'border border-secondary text-secondary hover:bg-secondary-light',
} as const

Two variants, not twelve: if you're tempted to add variant="primary-large-outlined", compose instead.

Headless Choices

For interactive primitives, prefer unstyled/headless implementations styled with our tokens over heavyweight UI kits:

Need Reach for
Dialogs, dropdowns, popovers Radix primitives (headless, accessible) โ€” add per-feature, not wholesale
Forms Native elements + controlled inputs; zod validation shared with loaders (zod already a dependency)
Tabs/accordions Details/summary or minimal state โ€” only adopt a library on second use
Tables Plain table + Tailwind; TanStack Table only when sorting/paging complexity is real (tanstack)

Rationale: every UI kit imports its own styling opinions; headless keeps accessibility (focus traps, ARIA) while we own appearance. Adopt libraries one primitive at a time and record each adoption here.

Composition over Configuration

Prefer children/slots over boolean props:

// Prefer this:
<Card>
  <Card.Header>{post.title}</Card.Header>
  <Card.Body><Markdown source={post.content} /></Card.Body>
</Card>

// Over this:
<Card title={post.title} body={post.content} renderFooter={<Tags/>} compact={false} ... />

Heuristic: the third boolean prop is the smell. Boolean-prop combinations create states no designer approved and tests can't cover.

Server data flows down through props/loaders; components stay presentational. Mutations go through route actions/TanStack Query (tanstack), never inside deep child components fetching their own data โ€” that breaks SSR consistency and duplicates cache keys.

Practical Examples

Example 1: Extracting a repeated pattern

If /kb.index.tsx and /changelog.index.tsx both render article lists, extract ArticleList taking typed items โ€” not a generic "ListWidget" with config flags. Two concrete usages define the right abstraction better than speculation.

Example 2: Client-isolated widget

'use client';
import { useEffect, useState } from 'react'
export function ThemeToggle() { /* browser-only logic lives here */ }

Used from a server component like any other element; isolation stays at the leaf.

Example 3: Form with shared validation

const schema = z.object({ email: z.string().email() })
// same schema used by the server action/loader โ€” one definition, two validations
const result = schema.safeParse(formData)

Common Pitfalls & Troubleshooting

Problem Cause Fix
Same button looks different across pages Hand-rolled class strings drifting Use src/components/Button.tsx; add variant only if genuinely needed
Arbitrary values everywhere (bg-[#...]) Contributor didn't know tokens exist Point them to token list; reject arbitrary hex in review
Hydration mismatch warnings Browser-API access during server render Move to client component or useEffect
Prop-explosion wrapper Config-style API growth Refactor to compound/composed children
Duplicate fetch logic in components Data fetching below route level Hoist to route loader/service; pass via props or loaderData
Unused exported components accumulating No pruning habit Quarterly sweep: delete anything unreferenced (type-check catches removals)

Next Steps / Ops Actions

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse