[back to writing]

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.css file. 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 your globals.css file 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.json with design tokens defined under global.css  and how those tokens are used by the components under the path defined under aliases. components.

  • A typical global.css created 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-popover and color-popover-foreground are meant to be used in pairs as bg-popover and text-popover-foreground in the popover component.

  • If you make new atom component toobar you can define color-toolbar and color-toolbar-foreground to 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 selectors data-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

  1. 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.

  2. 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-nova to base-vega summarized:

    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.

  • “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.
  1. 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.