Skip to main content
Conceptual Palette Logic

Architecting Color Systems: A Process Comparison of Semantic vs. Perceptual Palette Construction

The moment a design system grows beyond a handful of components, color starts to feel like a liability. Teams find themselves asking: should we name colors by their function—'primary,' 'danger,' 'success'—or by their visual properties—'blue-500,' 'gray-200,' 'red-700'? The choice between semantic and perceptual palette construction isn't just a naming convention; it shapes how colors are selected, scaled, and maintained. This article compares both approaches at a conceptual level, focusing on workflow and process rather than tool-specific tips. We'll look at what each method assumes, where it breaks, and how to decide which one fits your project's reality. Why This Topic Matters Now Color systems have become a cornerstone of scalable design, but the industry is split. On one side, semantic palettes promise clarity for developers and content authors: a button's background is always `--color-primary`, regardless of how that color might shift over time.

The moment a design system grows beyond a handful of components, color starts to feel like a liability. Teams find themselves asking: should we name colors by their function—'primary,' 'danger,' 'success'—or by their visual properties—'blue-500,' 'gray-200,' 'red-700'? The choice between semantic and perceptual palette construction isn't just a naming convention; it shapes how colors are selected, scaled, and maintained. This article compares both approaches at a conceptual level, focusing on workflow and process rather than tool-specific tips. We'll look at what each method assumes, where it breaks, and how to decide which one fits your project's reality.

Why This Topic Matters Now

Color systems have become a cornerstone of scalable design, but the industry is split. On one side, semantic palettes promise clarity for developers and content authors: a button's background is always `--color-primary`, regardless of how that color might shift over time. On the other side, perceptual palettes offer a more systematic way to generate accessible, harmonious scales—think of Tailwind's color tiers or Material Design's tonal palettes. The stakes are high: a poorly chosen color architecture can lead to inconsistent UI, accessibility failures, and maintenance nightmares that surface months after launch.

Consider a common scenario: your team is redesigning a SaaS dashboard that already has hundreds of components. The current color system uses a mix of hardcoded hex values and a few CSS custom properties. Designers complain that adding a new semantic role (like 'warning-soft') requires guesswork, while developers worry about accessibility contrast ratios. This tension is exactly where the semantic vs. perceptual debate lives. A semantic system might define colors by their job—'info,' 'success,' 'error'—but it doesn't inherently guide you toward a visually balanced scale. A perceptual system, on the other hand, gives you a ladder of lightness steps, but mapping those steps to real UI roles can feel arbitrary.

We're seeing more design tools and frameworks push toward perceptual methods. Tailwind CSS popularized the numbered scale (50–900), and Adobe's Leonardo tool generates accessible color ramps based on perceptual uniformity. Yet many enterprise design systems—like those at Salesforce or IBM—still rely heavily on semantic naming, especially when the system must be translated across brands or themes. The question isn't which is better in the abstract; it's which process aligns with your team's workflow, tooling, and tolerance for abstraction.

Who Should Read This

This article is for design system engineers, UI designers, and front-end developers who are evaluating or revising their color architecture. If you've ever stared at a color ramp and wondered why the step from 400 to 500 feels too drastic, or why a semantic token like 'danger' doesn't work well on dark mode, you'll find practical criteria here. We assume you're familiar with basic color models (HSL, LCH) and have some experience with design tokens, but we'll define terms as we go.

Core Idea in Plain Language

At its heart, the difference between semantic and perceptual palette construction is about what you optimize for. Semantic palettes optimize for meaning: each color token has a name that describes its role in the interface. For example, `--color-primary` might map to a blue hue for buttons and links, while `--color-danger` maps to red for destructive actions. The palette is built around these roles, and the actual hue, saturation, and lightness are chosen to fit each role's context. If you need a new role, you add a new token and pick a color that feels right for that job.

Perceptual palettes optimize for visual consistency. They start with a set of base hues and then generate a scale of lightness steps (often 10–15 levels) that are perceptually even—meaning the difference between step 100 and 200 looks the same as the difference between 500 and 600. The color tokens are then derived from these scales, often with names like `--blue-500` or `--gray-200`. Semantic roles are assigned later, by mapping a role token to one of these perceptual steps: `--color-primary` might be `--blue-500`, and `--color-danger` might be `--red-600`.

The catch is that these two goals—meaning and visual consistency—can conflict. A semantic token like 'success' might need to work on both light and dark backgrounds, which requires a color that maintains contrast in both contexts. A perceptual scale gives you fine-grained control over lightness, but the same step number might not work for every role. For instance, `--green-500` might be perfect for a success button on a light background but fail contrast on a dark background, where you'd need `--green-300` or `--green-700`. The semantic approach would solve this by creating two tokens: `--color-success-light` and `--color-success-dark`. The perceptual approach might stick with one token but accept that it won't be optimal in both contexts.

Why the Distinction Matters

The choice affects every downstream decision: how you generate color ramps, how you document them, how developers consume them, and how easy it is to rebrand. Semantic palettes are often easier for non-designers to understand—'danger' is more intuitive than 'red-600'—but they can lead to a proliferation of tokens as you account for every state and variant. Perceptual palettes are more systematic and easier to generate programmatically, but they require a mapping layer that can feel abstract to stakeholders.

How It Works Under the Hood

Let's look at the mechanics of each approach, focusing on the color science and token structure. We'll use the LCH color space as a reference because it's perceptually uniform—a key concept for perceptual palettes—but the same principles apply to HSL or OKLCH.

Semantic Palette Construction

A semantic palette starts with a list of roles. Typical roles include: primary, secondary, accent, neutral, success, warning, danger, info. For each role, you define a set of states: default, hover, active, disabled, and sometimes text on that color. The process is largely manual: a designer picks a base hue for each role, then adjusts lightness and saturation for each state, often by eye or with a contrast checker. The result is a set of tokens like:

  • `--color-primary`: #0066CC
  • `--color-primary-hover`: #0052A3
  • `--color-primary-active`: #003D7A
  • `--color-primary-disabled`: #B3D4F0

The advantage is that each token has a clear purpose. The disadvantage is that there's no systematic relationship between roles. If you later decide to shift your brand color from blue to green, you have to manually update every token for primary, and then re-evaluate whether the hover/active steps still look right. There's no underlying scale to guide you.

Perceptual Palette Construction

A perceptual palette starts with a set of base hues and a target number of steps. Using a tool like Leonardo or a script that interpolates in a perceptually uniform space, you generate a ramp of colors that are evenly spaced in lightness (and optionally chroma). For example, a blue ramp might have 11 steps from very light (50) to very dark (900). Each step has a known lightness value, typically in the range 0–100 in LCH. The tokens look like:

  • `--blue-50`: #E3F2FD (lightness ~95)
  • `--blue-100`: #BBDEFB (lightness ~85)
  • ... up to `--blue-900`: #0D47A1 (lightness ~15)

Then you create a mapping file that assigns semantic roles to these steps. For example:

  • `--color-primary`: var(--blue-600)
  • `--color-primary-hover`: var(--blue-700)
  • `--color-primary-active`: var(--blue-800)
  • `--color-primary-disabled`: var(--blue-200)

The advantage is that the ramp is mathematically consistent. Changing the brand color means generating a new ramp from a different hue, and the mapping file stays the same. The disadvantage is that the ramp's steps may not correspond perfectly to the visual needs of each role. For instance, `--blue-200` might be too dark for a disabled state on a white background, or `--blue-800` might not have enough contrast with black text. You then have to adjust the mapping or create custom tokens that deviate from the ramp.

Hybrid Approaches

Many teams end up with a hybrid: they use perceptual ramps for neutrals and a few key hues, but fall back to semantic naming for roles that need custom colors. For example, a 'danger' role might use a red ramp, but the specific step for 'danger-hover' might be hand-picked because the ramp's step doesn't meet contrast. This hybrid approach can work well, but it requires clear documentation to avoid confusion.

Worked Example or Walkthrough

Let's walk through a concrete scenario: a team building a design system for a project management app. They need a color system that supports light and dark modes, covers about 20 components, and will be used by three developers and two designers. We'll compare how each approach handles the same requirements.

Semantic Approach

The team starts by listing roles: primary (for main actions), secondary (for secondary buttons), neutral (for backgrounds and text), success, warning, danger, and info. For each role, they define a base color and state variations. They work in HSL, adjusting lightness manually. After a few rounds of feedback, they end up with 28 tokens. The primary token is a blue (#1A73E8), and they derive hover (#1558B0) by reducing lightness by 10%, active (#0F3D78) by another 10%, and disabled (#A8C7FA) by increasing lightness and reducing saturation. The process takes about two weeks, with multiple design reviews.

When dark mode is added, they realize that the same tokens don't work. The primary blue on a dark background looks too bright. They create a dark-mode override: `--color-primary-dark` (#4A90D9) and adjust all state tokens again. The token count doubles to 56. Six months later, a new designer joins and asks why `--color-primary-hover` is different from `--color-primary-dark-hover`. The documentation is thin, and the reasoning is lost.

Perceptual Approach

The same team starts by choosing three base hues: blue (primary), green (success), and red (danger). They generate 11-step ramps for each using Leonardo, targeting a lightness range of 10–95 with even steps. They also generate a neutral ramp from white to black. The ramps give them 33 hue tokens plus 11 neutrals. Then they create a mapping file for light mode: primary button gets blue-600, hover gets blue-700, active gets blue-800, disabled gets blue-200. Success gets green-500, and so on. The mapping takes a few hours.

For dark mode, they create a separate mapping file: primary button gets blue-300, hover gets blue-200, active gets blue-100, disabled gets blue-700. The ramp ensures that each step has a predictable lightness, so they can quickly verify contrast ratios. The team now has 44 hue tokens (the ramps) plus two mapping files. When they need to add a new component, they pick a step from the ramp and add it to the mapping. The process is faster, but they hit a snag: the warning role needs a yellow-orange hue that isn't in their base set. They generate a new ramp for yellow-orange, adding 11 more tokens. The system grows but remains systematic.

Trade-offs in This Example

The semantic approach was faster initially but harder to maintain. The perceptual approach required more upfront work (generating ramps) but paid off in consistency and dark-mode support. However, the perceptual approach also introduced a new cost: the mapping layer. Developers had to remember that `--color-primary` is not a color but a variable that points to another variable. Debugging required tracing through two levels of indirection.

Edge Cases and Exceptions

No approach is flawless. Here are common edge cases where each method struggles.

Accessibility Overrides

Semantic palettes often fail when you need to meet WCAG AA or AAA contrast ratios across multiple backgrounds. A single semantic token like `--color-primary` might work on white but fail on a light gray background. The fix is usually to create more tokens (e.g., `--color-primary-on-light`, `--color-primary-on-dark`), which multiplies the palette. Perceptual palettes handle this better because you can choose a step with sufficient contrast for each context, but the mapping becomes complex if you have many background variations.

Brand Changes

Rebranding is painful for semantic palettes. If your brand shifts from blue to purple, you must manually update every primary token and its states. With a perceptual palette, you generate a new purple ramp and update the mapping—if the mapping was done correctly. But if you had custom overrides for specific components (e.g., a button that uses a non-ramp color), those overrides are brittle.

Multi-Brand Systems

For design systems that serve multiple brands (like a white-label platform), semantic palettes can be easier because each brand maps its own colors to the same role tokens. Perceptual palettes require each brand to have its own set of ramps, which can be generated programmatically but still needs a mapping layer. The trade-off is between flexibility (semantic) and systematic generation (perceptual).

Data Visualization Colors

Charts and graphs often need categorical colors that are distinct but not tied to UI roles. Perceptual palettes shine here because you can pick evenly spaced hues from a perceptually uniform space. Semantic palettes would require a separate set of tokens for 'chart-1', 'chart-2', etc., which is essentially a perceptual palette in disguise.

Limits of the Approach

Both methods have inherent limitations that aren't solved by better naming or tooling.

Perceptual Uniformity Is an Approximation

No color space is perfectly uniform for all contexts. LCH and OKLCH are close, but they still have quirks—especially in the blue and purple regions, where hue shifts can be noticeable. A ramp generated algorithmically might look uneven to the human eye, requiring manual tweaks. This defeats the purpose of a systematic approach.

Semantic Drift Over Time

Semantic palettes tend to accumulate exceptions. A designer adds a new role like 'info-soft' because the existing 'info' is too strong. Over months, the palette grows without a coherent structure. The original intent is lost, and new team members struggle to understand why certain tokens exist. Perceptual palettes are more resistant to drift because the ramp structure imposes discipline, but they can still accrue custom overrides that break the system.

Tooling Mismatch

Many design tools (Figma, Sketch) don't natively support perceptual ramps. You can use plugins or external tools to generate them, but the handoff between design and development can be messy. Semantic palettes are simpler to document in design tools because each token is a named color. Perceptual palettes require designers to understand the ramp concept and use the same step numbers as developers, which can be a communication challenge.

Performance and Bundle Size

Perceptual palettes often generate more tokens (e.g., 11 steps per hue × 5 hues = 55 tokens) compared to a semantic palette that might have 20–30 tokens. This can increase CSS file size, especially if every token is defined as a custom property. However, this is rarely a bottleneck in modern web apps. The bigger issue is cognitive load: developers have to navigate a larger set of variables.

Reader FAQ

Which approach is better for a small team? For a team of 2–3 people building a simple product, a semantic palette is usually sufficient. You don't need the overhead of ramps and mapping. But if you plan to scale or support multiple themes, invest in a perceptual approach early.

Can I switch from semantic to perceptual later? Yes, but it's painful. You'll need to remap every token and update all component references. It's often easier to start with a perceptual base and add semantic aliases on top.

What tools can help generate perceptual ramps? Adobe Leonardo is a free web tool that generates accessible color ramps in LCH. There are also npm packages like 'color2k' and 'polished' that can help with interpolation. For design, plugins like 'Color Box' for Figma can generate ramps.

Should I use HSL or LCH for my ramps? LCH (or OKLCH) is better for perceptual uniformity, but HSL is more widely supported in CSS. You can generate ramps in LCH and convert to sRGB for production. Many modern browsers support LCH in CSS, but fallbacks are still recommended.

How do I handle dark mode with a perceptual palette? Create a separate mapping file for dark mode that points to different steps in the same ramps. For example, a primary button might use step 600 in light mode and step 300 in dark mode. The ramp ensures that each step has a predictable lightness, making contrast checks easier.

What about accessibility? Both approaches can meet WCAG standards if you test each token. Perceptual ramps make it easier to choose steps with sufficient contrast, but you still need to verify against your specific background colors. Use tools like Stark or the WebAIM contrast checker.

Is there a middle ground? Yes. Many teams use a hybrid: generate perceptual ramps for neutrals and a few key hues, but use semantic naming for roles that don't fit the ramp (like error states that need a specific red). Document the exceptions clearly.

Next Steps: Start by auditing your current color system. Count the number of tokens and note how many are direct hex values versus variables. Identify pain points—are you spending too much time adjusting colors for dark mode? Are new components inconsistent? Then run a small experiment: generate a perceptual ramp for one hue (e.g., your primary) and try to map your existing semantic tokens to it. See how many fit and how many need custom overrides. This will give you a concrete sense of the migration effort. Finally, involve your developers early. The mapping layer can be confusing, so pair with them to define the token structure before committing to a full rebuild.

Share this article:

Comments (0)

No comments yet. Be the first to comment!