Built a tiny design system of primitives to keep forms, tables and cards consistent. Years later, ShadCN validated the approach with a great developer experience.


When I wrote that post, I was reflecting on something that quietly changed how I structure almost every app: building a design system of primitives early — before the UI needs explode into thousands of pages. Looking back, the fact that something like ShadCN UI (and its ecosystem) exists today simply affirms the path I chose.

Why primitives matter

In many early-stage apps I saw a cascade problem: one style guide → two components → five pages → ten developers → “why does this button look different again?” By defining basic primitives — cards, form fields, tables, modals — you create a single source of truth for UI behaviour. It means when you update a primitive (e.g., input field behaviour), all forms shift synchronously.

Years later, posts about “design-systems in React” emphasise this kind of discipline. For example, sources highlight that building the UI as composable primitives leads to “fewer surprises, consistent developer experience, faster onboarding”.

How I built mine

Here’s a sketch of how I started:

// primitives/Button.tsx
import { FC, ReactNode } from “react”;

type ButtonProps = {
children: ReactNode;
variant?: “primary” | “secondary” | “tertiary”;
onClick?: () => void;
};

export const Button: FC<ButtonProps> = ({
children,
variant = “primary”,
onClick,
}) => (
<button
className={`btn btn‐${variant}`}
onClick={onClick}
>
{children}
</button>
);

// primitives/Input.tsx
type InputProps = {
label: string;
name: string;
value: string;
onChange: (val: string) => void;
};

export const Input = ({ label, name, value, onChange }: InputProps) => (
<div className=”form-field”>
<label htmlFor={name}>{label}</label>
<input
id={name}
name={name}
value={value}
onChange={e => onChange(e.target.value)}
className=”input”
/>
</div>
);


Then in an app screen:

import { Button, Input } from “@/primitives”;

const LoginForm = () => {
const [username, setUsername] = useState(“”);
const [password, setPassword] = useState(“”);
return (
<>
<Input label=”Username” name=”username” value={username} onChange={setUsername} />
<Input label=”Password” name=”password” value={password} onChange={setPassword} />
<Button variant=”primary” onClick={handleLogin}>Log in</Button>
</>
);
};

By doing this, I avoided “one-off” styles creeping in and created consistency that scaled.

Real-world context & relevance

  • Design-system thinking is now common in major frontend organisations: building UI as components and primitives leads to maintainable, scalable systems.

  • A 2025 article on PWA 2.0 emphasises that beyond offline-support etc, UI/UX consistency is now critical: “Apps must feel cohesive and fluent across devices” — and design primitives help deliver that. dazzlebirds.com+2PixelFreeStudio Blog –+2

  • The shift from “UI pages” to “component ecosystems” aligns with my earlier work: instead of ad-hoc screens, think “library of building blocks” + “compose screens”.

What this meant for me & my projects

  • In projects like Quietscribe, Key2Key – Workshop Manager and TradieScore, the early investment in primitives paid off: new features (forms, dashboards, tables) simply reused the same building blocks, meaning less rework, less visual drift and faster iteration.

  • Developer onboarding became smoother: once someone understood the primitives, they could build pages by composition rather than reinventing styles.

  • UI maintenance dropped dramatically: when design reviewed a change, updating the primitive cascaded automatically.

  • The “ShadCN-esque” comment reflects that my earlier system anticipated the modern “Tailwind + component library” approach: minimal styling, maximal reuse.

Looking ahead

  • I’m now thinking in terms of primitive + variant + theme: the UI library still needs to adapt, but the core abstractions stay stable.

  • For PWAs and offline-first apps (which you build so often), the UI must handle state transitions (online → offline, sync pending, error zones) gracefully — and that means primitives for those states as well (e.g., OfflineBanner, SyncIndicator).

  • As collaboration features grow (teams, shared memory, diarization, etc), UI primitives will need to support states over time (e.g., annotationCard, speakerLabel, versionBadge).

  • The next step is tokenised design systems (colour tokens, spacing tokens, semantic tokens) embedded into the primitives so you can theme apps for different domains (repair shops vs insurer dashboards) without rewriting the UI.

Final thought

The real work of a user interface isn’t in creating “beautiful pages” — it’s in designing for evolution. When you build your primitives first, your system can shift, scale and survive. I like building early, small, consistent. And when the ecosystem (like ShadCN) later validates that pattern — well, that’s just confirmation I was on the right track.