Go somewhere.

Home Page About Page Projects Page CV Page Dev Page Journal Page Contact Page Astro for Documentation and a Professional Site Dev Codex anywhere with tmux, Mosh, Termius, and Tailscale Dev Observing Codex MCP Tool Calls with Langfuse Dev Localization in Product Apps Dev MCP as a Safe AI Integration Boundary Dev Zod, OpenAPI, and Swagger for API Contracts Dev pg-boss for Durable Background Jobs Dev pgvector and RAG, Explained Through a Real Knowledge Workflow Dev Pragmatic Drag and Drop for Real Ordering Tasks Dev Prisma and PostgreSQL as the Product Source of Truth Dev Ralph Loop as a Plan Queue Dev React Router for Full-Stack Product Workflows Dev shadcn-Style UI as an Owned Product System Dev Dense Operational UI with Tables and Editors Dev Terminal Spike: a Native Android Terminal Dev Input Chinese pinyin with tones on Linux with fcitx Dev How to configure the DEFT Pro trackball on Linux Dev Use Pocket (read it later) on KOReader Dev Vercel AI SDK with Explicit Tool Boundaries Dev Vertical Slice Architecture with Dependency-Cruiser Dev Testing Product Workflows with Vitest and Playwright Dev Zod Beyond Validation Dev The Brothers Karamazov is the best novel I have ever read Journal Oral history of two Christians in a Chinese labour camp in the 1960s Journal The Word in the Bible Journal We love because He first loved us, not meaningless self-love Journal Mom, let me take the blame for Dad's mistakes Journal My story with God Journal The world is far from God, close to China Journal The Five Love Languages and Extrovert Only Exist in Language Journal Personality types don't exist, life is not a matching game Journal Dangerous words, why psychology is impossible Journal The story of a Shenzhen worker in 2000 Journal A Conversation with a Driver in Ras Al Khaimah Journal Shadian: a 1975 conflict between Communist forces and Muslims in China Journal Documentaries About China Journal
← Dev/Engineering

shadcn-Style UI as an Owned Product System

Why copied UI primitives can become a practical product system instead of a black-box dependency.

I like copied UI primitives because they make the component library feel like part of the app, not something the app is borrowing.

That is the part of the shadcn/ui-style approach I find most useful. The components are not locked away behind a black-box package. They live in the codebase. The team can read them, change them, and make them fit the product language.

This matters most for repeated product patterns: dialogs, forms, tables, empty states, toasts, and menus. The visual style should be consistent, but the copy and behavior still need to match the action.

A delete dialog and a submit dialog may share the same primitive. They should not share vague language.

Dialog primitive: shared
Action copy: specific
Permission check: owned by the workflow
Result handling: owned by the workflow

The component system should make the common shape easy. It should not hide the product decision.

For example, a confirmation dialog can standardize layout, focus behavior, buttons, and accessible labels. The workflow still decides what is being confirmed, whether the user has permission, and what happens after confirmation.

That boundary keeps shared UI useful without making it too clever. A component named ConfirmDialog can be shared. A component that secretly knows every domain-specific action becomes harder to reuse and harder to trust.

The trade-off is ownership. Copied components are now your components. If accessibility needs improvement, the team owns that improvement. If the design changes, the team owns the update. That is more work than importing a finished library and accepting its defaults.

I think that trade-off is often good for product apps. The app will usually need its own language anyway. Empty states need to explain what the user can do next. Toasts need to say what actually happened. Form errors need to match the workflow. A generic component library cannot decide those things for the product.

Icons are similar. A consistent icon set is useful, but icons should support recognition, not replace clarity. A destructive action still needs careful text. A toolbar button still needs an accessible label or tooltip if the icon is not obvious.

I would use this style when:

  • the app has repeated interaction patterns
  • the team wants control over details
  • accessibility and copy need product-specific review
  • consistency matters more than fast visual novelty

I would be more cautious for a very small prototype where a full component system would slow everything down. I would also avoid turning every shared component into a large abstraction. Sometimes a copied primitive with clear props is enough.

The value is not that the UI looks like a certain library. The value is that common product interactions become consistent and editable. A good product system should make the ordinary paths feel predictable, while still leaving enough room for each workflow to say exactly what it means.