---
title: "Style presets vs hand-tuned CSS in shadcn projects"
description: "What a shadcn style preset carries, what hand-editing globals.css costs you over time, and how to decide which one your marketing site should run on."
slug: style-presets-vs-hand-tuned-css
date: "2026-08-25"
tags:
  - shadcn
  - ai-workflow
author: Initium Team
---

Every shadcn project reaches the same fork. You have components installed, the default palette looks like the default palette, and you need the site to look like yours. One path is opening `globals.css` and editing tokens until the preview matches what you had in mind. The other is installing a style preset that arrives with those decisions already made and already consistent.

Both produce a themed site. They differ in what happens on the second day, when a new block lands, a dark mode bug appears, or an agent edits a page you did not write.

## What is a style preset in a shadcn project?

A style preset is a registry item that carries theme decisions instead of a component. In the shadcn registry schema it is typed `registry:style` or `registry:theme`, and its payload is mostly CSS variables rather than TSX ([registry-item.json docs](https://ui.shadcn.com/docs/registry/registry-item-json)).

The variables live under a `cssVars` field split three ways: `theme` for base tokens, `light` for the `:root` values, and `dark` for the `.dark` overrides. The registry docs show the shape directly:

```json
{
  "name": "custom-theme",
  "type": "registry:theme",
  "cssVars": {
    "theme": { "font-heading": "Poppins, sans-serif" },
    "light": { "brand": "20 14.3% 4.1%", "radius": "0.5rem" },
    "dark": { "brand": "20 14.3% 4.1%" }
  }
}
```

A style item can also carry a `css` field, which writes real rules into `@layer base`, `@layer components`, `@utility`, or `@keyframes` ([registry examples](https://ui.shadcn.com/docs/registry/examples)). That matters more than it sounds, because a visual system is not only colors. It is also what an `h1` does at the base layer, what a shared `.wrapper` utility sets, and how a tagline is cased and weighted.

When you install one, the CLI writes those values into your project's CSS file. The registry FAQ puts the mechanism plainly: to add a new color you add it to `cssVars` under `light` and `dark`, and "the CLI will update the project CSS file" so the new colors become available as utility classes ([registry FAQ](https://ui.shadcn.com/docs/registry/faq)). Nothing is hidden in a dependency. The tokens end up in your repo, in a file you can keep editing.

## What does hand-tuning globals.css actually cost?

Hand-tuning is not wrong. It is a maintenance shape.

shadcn's theme system is built on semantic pairs: a base token controls a surface, and the matching `-foreground` token controls text and icons on that surface, with the same names redefined inside a `.dark` selector for dark mode ([theming docs](https://ui.shadcn.com/docs/theming)). Since the Tailwind v4 alignment, those values are written in OKLCH and exposed to utilities through an `@theme inline` block ([Tailwind v4 guide](https://ui.shadcn.com/docs/tailwind-v4)).

That is a well-designed system, and it is also a lot of paired edits. Every color you touch by hand has at least four correct places to be touched: the base token and its foreground, in light and in dark. Miss one and nothing breaks loudly. You get a card that reads fine until someone toggles the theme, or a muted surface that drifted two edits away from the one next to it.

The second cost is scope creep in the wrong file. Radius is the clearest example. shadcn derives a whole scale from a single `--radius`, with smaller radii scaling down and larger ones scaling up, so changing that one variable moves the entire shape language ([theming docs](https://ui.shadcn.com/docs/theming)). Once someone hardcodes `rounded-xl` on a card because it looked better that afternoon, the scale no longer describes the site. The token is still there. It has stopped being the source of truth.

Tailwind v4 makes the same point from the other side. Theme variables are not ordinary CSS variables: they define which utilities exist. The Tailwind docs are explicit that theme variables do more than hold values: they instruct Tailwind to create the utility classes you write in your markup ([Tailwind theme docs](https://tailwindcss.com/docs/theme)). Editing them by hand is editing your utility vocabulary, one line at a time, usually at the end of a long day.

## What does a preset carry that a color picker does not?

Picking a primary color is the easy part of a visual direction, and it is the part most theming flows stop at. The decisions that make a marketing page look designed sit slightly below the palette:

- Type scale, and the relationship between a hero heading, a section heading, and body copy.
- Heading and body font families, with the specific weights that pairing needs.
- Radius scale, applied consistently across cards, buttons, forms, and containers.
- Button treatment, including the weight of button text, which is set inside the component rather than in a token by default.
- Form field treatment, so inputs match buttons instead of approximating them.
- Marketing texture: what a tagline looks like, how body sections set their measure.

In Initium the preset is a `registry:style` item that carries all of that together. It writes `:root` and `.dark` token blocks, adds base-layer rules for headings, wrapper, tagline, and paragraph utilities, ships the button and form components already tuned to the chosen weight, and pulls the fonts in as registry dependencies rather than leaving you a font import to remember. The [styles documentation](/docs/styles) covers the full contents.

The practical effect is that the decisions arrive as a set. A palette plus a mismatched radius plus a default button weight is three unrelated choices sitting on one page, which is exactly what "looks generated" describes.

## How do you install one?

Install the style before any blocks, because blocks depend on the shared utilities the style defines. The base pack:

```bash
npx shadcn@latest add @initium/initium-styles
```

Or a preset exported from the Style Builder, which encodes your specific selections in the URL:

```bash
npx shadcn@latest add "https://app.initium.sh/r/preset?code=..."
```

The `code` value is generated. It is not meant to be edited by hand. If the `@initium` namespace prefix is unfamiliar, the mechanics of namespaces and what an install actually writes are covered in [shadcn registries explained](/blog/shadcn-registries-explained).

Blocks come after:

```bash
npx shadcn@latest add @initium/hero-section-1
```

## What should you check after installing a preset?

A preset writes to files you own, which means the install is a diff worth reading rather than a black box worth trusting.

Check that both token blocks landed. Your CSS file should have the new values in `:root` and the matching set inside `.dark`. A preset that appears to work in light mode and reverts to defaults in dark usually means only half the tokens were written.

Check the `@theme inline` mapping. Tokens that are not exposed through it exist as CSS variables but generate no utilities, so `bg-brand` will not do anything even though `--brand` is defined.

Check that the preset landed once. If you had already hand-edited tokens, you can end up with two definitions of the same variable in one file and the later one silently winning. Delete the stale block rather than layering over it.

Then look at a real page, in both modes, at a small breakpoint. Presets are theory until a hero section with a tagline, a heading, body copy, and two buttons renders all at once.

## When is hand-tuning still the right call?

When the change is genuinely local and genuinely one-off. A single section that needs a different background is a one-line utility, not a token. Prototypes are another fair case: if the site will be rewritten next week, the system you are protecting does not exist yet.

The line worth holding is between adjusting a token and bypassing it. Changing `--radius` because the shape language should shift is the system working. Hardcoding a radius on one card because that card needed it is the beginning of a site with no shape language at all.

## Why does this matter more with an agent in the loop?

An agent editing your marketing page has no visual memory. It reads the code it can see and continues the patterns it finds. If your styling lives in tokens, the patterns it finds are correct by construction: it writes `bg-card`, `rounded-lg`, `text-muted-foreground`, and the result matches everything around it.

If your styling lives in accumulated one-off overrides, the agent learns the overrides. It sees a hardcoded radius and treats it as the house style, then reproduces it in the next three sections. Drift compounds faster with an agent than without one, precisely because the agent is consistent about whatever it was shown.

This is why a style preset pairs naturally with the rest of the system. Tokens tell the agent what the site is made of. A `DESIGN.md` export tells it which of those decisions were intentional. Skills tell it how to use blocks without breaking either.

If you have been theming by hand and the site has started drifting, the fastest reset is to export a preset that matches the direction you actually want, install it before your next block, and let the tokens do the arguing. The [Style Builder and styles docs](/docs/styles) are the place to start.
