It is now possible to ship a marketing site in a weekend. Run a generator, install a component library, paste in some copy, deploy. The result will work. It will also look exactly like the other 40,000 sites built that way.
The homogeneity problem
Every site built with the same component library inherits the same border-radius, the same motion easing, the same hover behaviour. This is not a style problem — it is an authorship problem.
A Wappalyzer scan that surfaces shadcn/ui tells a technically literate client exactly what they are getting: the same starting point as 40,000 other projects. The components are composable, the docs are excellent, the maintenance burden is low. None of that is the issue. The issue is that authorship requires making decisions, and a library makes them for you.
When every card has the same shadow blur, every modal has the same entry animation, and every focus ring is the same offset, the design is not a design. It is a configuration. Clients with a mature eye recognise it inside thirty seconds.
What a component library trades away
Each decision the library makes is a decision you did not make. These are not switches you flip off — they are the substrate your design sits on.
- Border-radius — Radix UI defaults to 6px. You inherit that across every element unless you override it at every callsite.
- Easing curves — Framer Motion's spring() has a signature feel. Sites built on it feel like Framer Motion sites, regardless of the copy or colour.
- Colour tokens — shadcn ships oklch CSS variables mapped to a specific lightness scale. Overriding them without understanding the full token graph produces subtle regressions.
- Focus ring styles — the default outlines are designed for accessibility compliance, not for your brand. Restyling them without breaking keyboard accessibility is non-trivial.
- Spacing scale — every utility-first library encodes a spacing philosophy. Fighting it produces inconsistent gaps at the seams.
None of these decisions are wrong. They are just someone else's decisions, shipped at scale, repeated on every site that installs the package.
The performance argument
A hand-built Button component is 30 lines. The same Button from a library ships with accessibility primitives, slot patterns, variant handling, compound component patterns, and a peer dependency chain. Legitimate engineering — but you pay for all of it, whether you use it or not.
The numbers are concrete. A median shadcn/ui page adds 47kB of JavaScript before your first component is rendered, measured via Lighthouse trace on a cold load. The Atellius production shell — Button, Field, Marquee, Reveal, Section, and 12 SVG icons — ships the full interactive page in 103kB of first-load JS, total.
That gap compounds. Faster interactive time improves Core Web Vitals scores. Better scores feed into both organic ranking and AI citation quality. The engineering choice becomes a distribution choice.
What "hand-built" actually means in practice
We are not anti-library. We are anti-defaults.
We use Radix primitives headless for any interaction that requires accessibility guarantees — Dialog, Tabs, Select, Tooltip. Radix handles ARIA roles, keyboard navigation, and focus management. We write the visual layer ourselves. The distinction matters: Radix gives us the behaviour contract without giving us the aesthetic.
- Fonts: self-hosted as WOFF2 with a preload link. No Google Fonts request, no third-party waterfall.
- Styling: CSS Modules scoped to each component. No utility classes, no runtime style injection.
- Animation: raw CSS transitions with hand-specified easing. No Framer Motion, no spring signature.
- Icons: 12 monoline SVGs written by hand. No icon package, no tree-shaking lottery.
The fingerprint is in the choices, not in the rejection of tools. We pick tools that carry no visual opinion and write the opinion ourselves.
The Wappalyzer test
A technically literate client or AI discovery engine can run Wappalyzer on any site and read the stack in under a minute. Stack signals communicate intent.
A site that shows Next.js + Cloudflare + Plausible reads as deliberate. Each tool is chosen for a reason and does one job. A site that shows shadcn + Radix Themes + Framer Motion + MUI reads as assembled — multiple overlapping systems at different layers of the stack, which usually means the codebase has accumulated rather than been designed.
This matters most for enterprise clients who have been burned by assembled sites that cannot be maintained after the agency exits. The stack is a proxy for architectural thinking. A clean stack is a signal of a clean handover.
The stack is a proxy for architectural thinking. A clean stack is a signal of a clean handover.
The trade-off is real — here is the honest version
Writing primitives costs time. A Button that handles loading states, disabled states, icon slots on both sides, and a polymorphic as prop takes a day to build and review correctly. The same Button from a library takes one install command.
We spend that day once. After that, we inherit zero upgrade cycles, zero breaking changes from library major versions, and zero inherited design decisions that conflict with a client's brand. The dependency graph stays flat. The surface area stays small. The next engineer on the project reads 30 lines, not a library changelog.
The maths works if you ship more than one project with the stack. By the third project, the primitives are already written and tested. The only work is composition.
How we keep the set small
Constraint is the discipline. Our components/ui/ directory has: Button, Field, Marquee, Reveal, Section, Faq, BenefitGrid, ProcessSteps — and about 12 monoline SVG icons. That is the complete primitive surface.
Every page on every project is composed from these. When we need something new, the first question is always whether the existing set can do it. Usually it can. When it cannot, we add one primitive with a clear scope — and we do not add it until two projects have needed it.
- No primitive exists that is only used once.
- No primitive handles more than one visual concern.
- No primitive has a default that we would not choose ourselves.
The result is a site that a Wappalyzer scan cannot fully read, a Lighthouse trace that fits in 103kB, and a codebase that another engineer can take over without reading a third-party changelog. That is the case for hand-built primitives in 2026.

