shadcn-Style UI as an Owned Product System

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.

Related Posts

Astro for Documentation and a Professional Site

I use Astro because this site is mostly writing. I do not need a heavy app framework for pages that should load fast and be easy to edit. That sounds simple, but it is the mai

read more

Localization in Product Apps

Localization is not only replacing English strings with another language. In a product app, language touches workflow. It changes labels, validation messages, dates, empty states, permissions copy, d

read more

MCP as a Safe AI Integration Boundary

MCP is interesting because it makes AI integrations feel less like prompt magic and more like software boundaries. That is the part I care about. A model should no

read more

Zod, OpenAPI, and Swagger for API Contracts

A public API is not just backend code. It is a product surface for another developer. That means the contract has to be readable. It also has to be enforced at runtime. Types in the app are useful, b

read more

pg-boss for Durable Background Jobs

The customer problem was not "we need a queue". The problem was that a slow operation made the user wait with no clear answer. That distinction matters. A queue is an implementation detail. The produ

read more

Pragmatic Drag and Drop for Real Ordering Tasks

Drag and drop is easy to add for a demo and harder to make reliable for real work. The product question is not "can the item move on screen?" The question is whether the user can safely change an ord

read more

Prisma and PostgreSQL as the Product Source of Truth

I do not think of PostgreSQL as only infrastructure. In a product app, it is where the product remembers what happened. That makes database design a product decision. I

read more

React Router for Full-Stack Product Workflows

A route is not only a URL. In a product app, a route often represents a task the user is trying to finish. That sounds obvious, but it changes how I design the code. A settings page that starts an im

read more

Dense Operational UI with Tables and Editors

Sometimes a simple form is the wrong UI. If the user needs to compare many values and make careful edits, a table can be kinder than a long page of inputs. Dense UI has a bad reputation when it is us

read more

Vercel AI SDK with Explicit Tool Boundaries

The risky part of an AI feature is not the chat UI. The risky part is what the chat is allowed to do. It is easy to make an assistant feel powerful by giving it tools. With something like the [Vercel

read more

Vertical Slice Architecture with Dependency-Cruiser

I like vertical slices because they make a feature easier to delete, move, or review. The folder structure is not the main value. The value is that the code for one workflow is not spread across ten u

read more

Testing Product Workflows with Vitest and Playwright

I do not want a test suite that only proves functions work. I want it to protect the workflows that would hurt if they broke. That does not mean every rule needs a browser test. Browser tests are val

read more

Zod Beyond Validation

Zod is usually introduced as a validation library. That is true, but the more useful idea is boundary definition. A TypeScript type only helps after data is already inside the pro

read more