Color Palettes: Quantity vs. Identity

Modern CSS frameworks have largely solved the problem of providing color palettes with multiple tones per hue. Tailwind CSS ships an extensive set of colors, Open Props offers up to 13 tones per color, and Pico CSS pushes that to 19. The numbers themselves don't matter much — what matters is having enough tonal range to create subtle visual effects across a design.

That said, you probably shouldn't use these palettes as-is. Sites built on Bootstrap look like Bootstrap; sites built on Tailwind look like Tailwind. Default palettes create visual sameness across the web. If you want your site to have its own identity, designing a custom palette is a meaningful first step.

Creating a palette isn't as daunting as the accessibility-focused writing on the topic suggests. The two words that really matter: sufficient contrast. That's the core requirement for accessible design.

Hand-Designing Palettes

Designing palettes by hand — for example, in Figma — can feel more natural than filling out every tone from 50 to 950. When you design that way, you pick colors that actually fit the layout rather than guessing at a full tonal range upfront.

The result is often a sparse set of variables that fit your specific needs. You might skip tones entirely and add variants that standard systems don't provide, like a d suffix for a desaturated version of a color alongside its standard tints and shades.

If you prefer generating palettes programmatically, tools like RampenSau, Perceptual Palettes, and Accessible Palette Generator are solid starting points that can be tweaked by hand afterward.

The Application Gap

Where palettes fall short is in application. Even systems that provide many tones tend to restrict how many you can actually use at the component level. DaisyUI supports roughly two tones per color. Pico CSS, despite comprehensive palettes, effectively exposes about two tones per component role — a single primary tone, a background and background hover, a border and border hover.

Given how much emphasis is placed on colors having multiple tones, this restriction is puzzling. Possible explanations: system designers may not be as visually sensitive as designers, semantic class naming creates confusion, or the values were intended as loose guidelines.

Semantic Conflation

The term "semantic" in these systems conflates two distinct ideas: an order of hierarchy (primary, secondary), and the element being styled (background, border). Hierarchy itself then splits further into color-specific order (primary means red) and use-case-specific order (primary describes a button's weight).

Naming gets hard precisely because these distinctions are collapsed. A cleaner separation would be:

  • Keep primary, secondary, tertiary for hue hierarchy.
  • Use verbs to describe component appearance: outline, light, heavy, ghost.

Looking at names like --pico-primary-background, the problems multiply: Does this mean there's only one main background color? What happens when the same component needs the same color for both background and border, but a different color for another component state? Numbering (background-1, background-2) compounds the confusion.

Dictionaries define semantics as "indicating by a sign" or "relating to meaning or logic." Nothing says that sign must be a word — numbers are semantic too. Everyone already understands that 100 is a light tint and 900 is a dark shade.

Porting hierarchy to hues directly unlocks vast combinations:

If pink is your primary color, orange can simply be secondary. This mapping can be manual in CSS, generated quickly with a Sass loop, or handled via a Tailwind plugin. Yes, it generates many CSS variables, but those variables weigh only bytes — far less than any image on the page. And the payoff is never having to guess whether it's background-1 or background-2; component code uses the semantic number directly.

Component-Level Semantics and Global Variables

Component-level semantics, like Pico CSS's class names, make practical sense. For simpler projects, dropping extra namespacing entirely reduces code volume while staying maintainable.

A separate consideration: some variables deserve to be global. If every component's border references a single --border-color, you can change it once and propagate the change site-wide. Global variables need to be baked into component definitions from the start. Tailwind utilities can serve as convenient Sass mixins to build reusable component patterns.

With global variables, theming a card is as simple as overriding --border-color — the component namespace is unneeded repetition. Other useful global values include focus ring styling, border radii, and shadows.

What Level of Rigor Do You Need?

The appropriate system depends entirely on your use case:

  • Single static theme? Skip the discussion and use the framework palette.
  • Multiple themes, simple design? Something like DaisyUI or Pico CSS may suffice.
  • Maximum flexibility and many color shades? A more robust palette-to-hierarchy system is necessary.

As Jason Cohen writes in "Rare things become common at scale": what works at small scale often becomes a liability as complexity grows. Take what serves your needs, and treat these ideas as territory to explore rather than prescriptive solutions.