What Shadcn/UI Actually Puts in Your Hands
The important question about shadcn/ui is not whether its components look consistent in a gallery. It is what a project can understand and change after a component has been installed. This chapter examines that boundary through Button, Dialog, and Sidebar: which decisions belong to the locally owned layer, which behavior remains in a primitive dependency, and how shared composition conventions and semantic tokens make editable source predictable without making every component identical.
That distinction is easy to miss if “open code” sounds like permission to edit a file and nothing more. A component is a small system with an interface, styling rules, state, and relationships to other components. Once that system lives in an application, the team owns more than its visible colors. It owns the choices that determine how callers compose it, how it responds to state, and how future changes will interact with the rest of the design system.
Predictability comes from a shared composition vocabulary
A shared interface is not the same as a shared implementation. In shadcn/ui, the interface is the part that lets a developer approach different components with similar expectations: ordinary element properties remain useful, variants have recognizable names, class composition accepts local additions, and component parts expose the states that styling needs. Those conventions reduce the amount of new knowledge required when a team moves from a button to a dialog or from a primitive to a larger feature.
The Button component is a compact example. It accepts the normal properties of a button, then adds named variant and size choices. A class-variance utility turns those choices into a consistent set of styles, while a shared class utility combines them with a caller's className. The component can also render through a slot when another element needs to carry the button's interface. In practical terms, the caller can keep using a familiar Button contract while choosing whether the rendered element should be the ordinary button or a composed child.
That design solves two problems at once. The variant names provide a stable way to express intent—default, destructive, outline, secondary, ghost, or link—without asking every caller to repeat a complete style recipe. The open source keeps the recipe visible. If a product's destructive action needs a different focus treatment, the team can see where that behavior is defined and decide whether to adjust the shared variant or introduce a local one. Predictability comes from the convention; ownership comes from being able to alter the implementation behind it.
The component also emits data attributes that identify its slot, variant, and size. These attributes are more than implementation decoration. They give CSS and surrounding tools a stable description of the component's state, so styling can respond to meaning rather than to a fragile arrangement of descendants. They also make the source easier to inspect: a developer can see which state a component believes it is in and which boundary a style is meant to target.
A Dialog shows why a common vocabulary must include composition, not only variants. It wraps a dialog primitive and exposes a family of parts: root, trigger, portal, overlay, content, title, description, and close. The primitive supplies general interaction behavior; the local layer decides how those parts look and how they fit the project's conventions. The content wrapper adds an overlay, positions the panel, applies borders and motion states, and includes a close control with a visually hidden label. The wrapper therefore contributes structure and semantics alongside styling.
The local Button can then be used as the dialog's close action. That is a small but important relationship. The Dialog does not invent an unrelated action style; it composes the same Button vocabulary the rest of the project uses. A team can change the Button's shared behavior and understand that the change may affect DialogFooter as well. Conversely, if a dialog needs a specialized close affordance, the source makes that relationship explicit enough to change deliberately.
This is what a primitive wrapper contributes beyond raw primitive behavior. It establishes defaults, connects the primitive to the design tokens, names the parts that application code will use, and creates a place for product-specific composition. It does not erase the primitive's existence. If the underlying dialog package changes its focus or portal behavior, the wrapper remains part of the integration surface that must be checked.
Semantic tokens are part of this architecture because they keep that integration surface coherent. A component can use background, foreground, primary, border, or ring as meanings rather than choosing an isolated color value. The theme maps those meanings to actual values, including different values in dark mode. A Button's primary variant and a Dialog's panel can therefore participate in the same visual system without sharing the same source file or hard-coding the same palette.
This separation has a useful consequence for local ownership. A project can revise the visual language by changing its token definitions instead of editing every component class. It can also make a component-specific decision when the semantic token is not enough. The token layer is a shared contract, not a lock. A developer may still add a new token, introduce a new variant, or deliberately break from the default. The important point is that the default relationship is visible and reusable.
The diagram's boundary is the central idea: a project-owned component is not an isolated copy of a primitive. It is an adapter and a design-system participant. It exposes a composition interface, consumes semantic tokens, and may combine several lower-level capabilities. The team can edit that adapter, but it must still understand the contracts on either side.
The Sidebar reveals the real cost of ownership
Button and Dialog can make open code look like a styling advantage. Sidebar shows the deeper consequence. It is not one visual surface with a few classes; it is a stateful system whose behavior crosses several boundaries. The source defines a provider, a context, desktop and mobile paths, nested menu parts, a trigger, a rail, tooltips, loading skeletons, and responsive layout decisions. Because the source is local, those decisions are visible. Because they are visible, they are also the project's responsibility.
The Sidebar provider owns whether the sidebar is open, can accept controlled state from outside, and tracks a separate mobile state. It exposes a small context interface to descendants, including the current state, setters, a mobile flag, and a toggle function. That ownership matters because it identifies where a transition begins and who is allowed to change it. A menu button does not need to know every detail of the layout; it asks the context to toggle. A product can control the provider from outside when it needs to, but the default behavior remains in the component system.
Persistence makes the boundary even clearer. When the open state changes, the component writes a cookie so the preference can survive navigation or a later visit. A keyboard shortcut also calls the same toggle path. These are not visual variants. They are product behavior with consequences for user expectations, browser state, and accessibility. If a team changes the persistence rule, the shortcut, or the controlled/uncontrolled behavior, it is modifying the way the application remembers and responds to user input.
Responsive behavior uses a separate route. On a mobile viewport, Sidebar presents its contents through a Sheet; on a larger viewport, it renders a fixed or collapsible desktop structure. The child content remains conceptually the same, but the interaction container changes. This is a good example of why “the component is just source code” is not enough as a mental model. Source ownership gives the team the ability to inspect and alter the decision, but a safe alteration requires understanding the state and layout assumptions that the decision connects.
The component also uses data attributes to expose state such as expanded or collapsed, the side on which it appears, its variant, and its collapsible mode. Those attributes allow the styling system to respond to the state without scattering conditional logic through every child. A menu label can hide or change opacity when the sidebar collapses; a tooltip can appear only when a collapsed desktop state makes the label unavailable; a rail can change its affordance according to side and state. The conventions carry behavior into styling, so the boundary between interaction and presentation is coordinated rather than accidental.
Sidebar is assembled from other locally recognizable parts: Button for the trigger, Input for search, Separator for divisions, Sheet for mobile presentation, Skeleton for loading states, and Tooltip for collapsed navigation. This composition gives the feature a common visual and interaction language. It also creates dependency relationships inside the project. Replacing the Button's focus treatment or the Sheet's close behavior may change what users experience in the Sidebar. The project can make that change, but it should treat the composed feature as a connected system rather than as a collection of unrelated files.
That is the limit of the simple copy-and-paste model. A copied visual primitive may be easy to understand in isolation. A stateful feature carries ownership of transitions, persistence, responsive fallbacks, and the interfaces between subcomponents. Even a modest local edit can alter a state machine: a new collapsed mode may require a new data attribute, a different keyboard label, a tooltip rule, and a mobile decision. The source is still approachable, but approachability does not mean that all changes are low risk.
The distinction helps locate different kinds of work. If every primary action needs a new color, the token layer may be the right place. If only a sidebar trigger needs a different size, a local variant or class may be enough. If the mobile panel cannot provide the required interaction, the change may involve the Sheet or its primitive dependency. If a new feature needs to share the same state and subparts, it may belong in the composition layer rather than as a one-off override. Open code makes these options available; the architecture helps decide among them.
It also explains why shadcn/ui's conventions matter to AI tools and to human teams. A tool that sees a Button with variants, data attributes, semantic tokens, and ordinary element props has more reliable signals for composing it than a tool that sees an opaque package API. A developer reviewing a Dialog or Sidebar can follow the same signals. Consistency is therefore not a visual promise imposed from above. It is a set of readable agreements that make local decisions easier to compare.
There is still a practical boundary. The examples demonstrate a pattern, not a guarantee that every item in every registry has the same quality or that every primitive exposes identical semantics. The first-party code provides evidence for these components, while the broader model remains a convention that projects and registries must maintain. A team should not infer that a familiar name means identical behavior across bases, versions, or custom registries.
Once the internal layers are clear, the next problem is delivery. A project cannot benefit from an owned component unless the platform knows what the item contains, which dependencies it needs, and where its files should go. The central question shifts from “What is inside this component?” to “How does an add request turn a registry description into this project's adapted source?”