understanding shadcn v4 semantics
design • UI • frontend
Shadcn recently released their shadcn/cli v4 CLI utility that allows you to customize your themes using presets and quickly try try fresh styles as the project is not backed in. While I’m excited on the simplicy of the abstraction and having a CLI to do the toil of copy pasting components manually to my directory, it left me with a lot of open ended questions.
Here are my notes on my discoveries on how to piece together tailwind utility classes, themes, imports and how they interplay with shadcn presets and themes:
-
Tailwind v4 is an entirely overhauled system that uses custom functions and directives exposed in our core
global.cssfile. These are not browser CSS standards by themselves; they are parsed by Tailwind during compilation. When installing Tailwind css as part of your framework, it will@import "tailwindcss";, a standard CSS to the top of yourglobals.cssfile with all the utility classes and then streamline them down to only what is used by the application at build time. -
Tailwind comes with default theme variables you can use out of the box and you can modify them the tailwind way; you define namespaces (think categories) and tailwind creates the underlying one or more utility class or variant APIs from your variable names.
@theme { --color-brand-500: oklch(0.7 0.15 250); } Tailwind sees --color-* and gives you color utilities like: - bg-brand-500 - text-brand-500 - border-brand-500@theme { --breakpoint-3xl: 120rem; } Tailwind sees --breakpoint-* and gives you a variant like: - 3xl:text-lg -
When you install shadcn themes based on a preset, the theme is define under
components.jsonwith design tokens defined underglobal.cssand how those tokens are used by the components under the path defined underaliases. components. -
A typical
global.csscreated by generated themes is of the following structure:@import "tailwindcss"; /* imports all tailwind utilities during development */ @import "tw-animate-css"; @import "shadcn/tailwind.css"; /* imports node_modules/shadcn/dist/tailwind.css */ @custom-variant dark (&:is(.dark *)); /* mapping of your shadcn css vars which change based on theme defined under :root or .dark to tailwind namespaces */ @theme inline { --font-heading: var(--font-sans); --font-sans: var(--font-sans); --color-sidebar-ring: var(--sidebar-ring); } /* based on preset */ :root { --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); } /* based on preset for dark mode */ .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); } -
Note the variables and namespaces are fairly minimal so you won’t have to learn many custom utilities. They are often mapped based on components so
color-popoverandcolor-popover-foregroundare meant to be used in pairs asbg-popoverandtext-popover-foregroundin the popover component. -
If you make new atom component toobar you can define
color-toolbarandcolor-toolbar-foregroundto keep the format consistent. This also saves you from not having to wory about poluting to many components if you need something hyper custom and easier to delete namespaces long term. To re-use styles you can do—color-toolbar: var(--background);if you want to use existing colors already defined. -
The
"shadcn/tailwind.css” file is not unique per preset and defines custom variants for how shadcn components can style CSS attribute selectorsdata-selected:bg-accent.@custom-variant data-selected { &:where([data-selected="true"]) { @slot; } }
Shadcn CLI
The CLI is independant helper that aids preset management and upgrading and managing components but it is entirely optional and you can continue to copy components manually by switching themes on the site. On upgrade you can choose to adopt only the underlying themes and/or also updating the underlying component.
/** To install a new preset WITH updating the @/components */
bunx --bun shadcn@latest init --preset b0 --force --reinstall
/** To install a new preset WIHTOUT updating the @/components */
bunx --bun shadcn@latest init --preset b0 --force --no-reinstall
/* then selectively update which componts you want by doing a dry-run/diff */
bunx --bun shadcn@latest add button --dry-run
bunx --bun shadcn@latest add button --diff components/ui/button.tsx
Frequenlty Ignored Asks
-
Should you change utilities on the components or the underlying namespace to modify the component?
The simplest is to update your namespace so it applies globally across all components but if you arne’t sure if you want to apply those styles to other components, the best route is to use suggestion above on how shadcn makes component specific variables. Magic tokens used one of such as text-[144px] are an antipattern so one option is to define them and then have llms do the translation into your @themes by defining the themes for you. Something to create a skill for.
-
How does “styles, tailwind.baseColor and theme” in the create interface impact the files?
- “Style” typically changes component anatomy and defaults. I ran a single test on button and saw no theme change. If you force component overrides here is a brief change from
base-novatobase-vegasummarized:
The updated button style shifts toward a cleaner, more conventional control: corners are slightly tighter (rounded-md instead of rounded-lg), heights and icon sizes are a touch larger for better presence and tap comfort, and the outline variant now has a subtle shadow for depth. Hover behaviour on the default variant is also simplified to a direct button hover state. Overall, it feels a bit sturdier and more balanced, with improved visual rhythm across sizes.
- “Style” typically changes component anatomy and defaults. I ran a single test on button and saw no theme change. If you force component overrides here is a brief change from
- “Base Color” setting changed the OKLCH color values light/dark mode. You can always use git blames to revert to historical versions of your themes or keep backups so it feels slightly non-destructive.
- “Theme” setting changed the primary, secondary and sidebar primary colors and no change to the button component. I didn’t notice a change to any other design color tokens from interface selector.
-
How do I make preset upgrades?
Ask your agent to do it but here is a general pattern. Upgrade preset non destructively and make changes to global css only. Then apply dry run against each component and check the diff to merge or force override.