component placement

why

placement is the cheapest ownership signal: the folder answers whether a component is a library primitive, a route's private detail, or a shared domain concept. three distinct places prevent primitives absorbing business logic and shared components being trapped in the route that created them.

rule

components/ui/ holds shadcn/ui components and directly library-derived primitives only. every authored product component lives outside it, under its actual domain or rendering responsibility.

next.js route boundary

  • app/ owns route segments, framework-required files, visible route composition — not a general component library.
  • route-private component: app/<route>/_components/.
  • used by more than one route: move to root components/<domain>/.
app/
process/
page.tsx
_components/
ProcessSummary.tsx
components/
process/
ProcessSummary.tsx

the two locations are mutually exclusive for an authored component. no app/components/; no shared component left in one route because that route created it first.

placement

  • shadcn-derived files stay in components/ui/ with ecosystem kebab-case names (alert-dialog.tsx, dropdown-menu.tsx).
  • authored shared components: components/<domain>/, named for domain + rendering responsibility — components/workflow/RunPanel.tsx, components/account/ProfileCard.tsx.
  • one-route component stays in that route's _components/ until another route proves it shared.
  • page composition stays in page.tsx; no one-use page wrapper extraction.

move decision

observed useplacement
one route renders it and no other route imports itthat route's _components/ folder.
two or more routes import the same authored componentroot components/<domain>/.
copied markup looks similar but behavior/vocabulary differskeep each route-private until a real shared responsibility is proven.
library-derived primitivecomponents/ui/ only.
framework route/layout/error/loading filerequired app/ location, not components/.

moving a component: update every import in one forward-only change, delete the route-private copy, no re-export shim at the old path.

this table is the component-scoped instance of the consumer-derived placement ladder in layered source organization; the same ladder governs non-component code.

do not

  • add a custom product component to components/ui/ because it looks primitive.
  • rewrite a library-derived component into a mixed product/business component.
  • create a generic component folder when a domain folder owns the behavior and vocabulary.
  • put a reusable authored component anywhere under app/; no app/components/ convenience folder.

verification

  • search imports before and after a move.
  • route-private component: callers only under its route.
  • shared component: no app/ implementation copy, imported through components/.
  • list app/: route segments, framework files, private _components/ folders only.

good and bad

bad

app/
process/
page.tsx // imports ProcessSummary
overview/
page.tsx // also imports ProcessSummary
components/
ProcessSummary.tsx // ...or components/ui/ProcessSummary.tsx

a shared component sits in the wrong directory (app/components or ui/). when another route needs ProcessSummary, the developer finds it at components/ui/ProcessSummary.tsx, assumes it's a primitive, and customizes it — process-specific styling, rendering logic, state. six months later ui/ProcessSummary mixes button styling (shadcn) with workflow logic (process domain). a button-only primitive update can't touch ProcessSummary without breaking workflow displays: refactoring unsafe, ui primitives polluted with business logic.

good

app/
process/
page.tsx // imports components/process/ProcessSummary
overview/
page.tsx // also imports components/process/ProcessSummary
components/
process/
ProcessSummary.tsx // shared by multiple process routes

placement answers one question — library-derived, route-private, or shared by a real domain — and the folder states it. ProcessSummary in components/process/ tells every developer it owns workflow display logic, not a primitive. primitives stay in components/ui/, business and design concerns stay separate, refactoring is safe.

search pages

go to any page