Skip to main content

Color system

Color is one of the strongest levers a merchant has to express their brand. A well-designed color system lets merchants restyle the entire storefront from theme settings — change one color, and every section follows. This guide shows how to build that system with color settings and CSS variables.

Define color settings by role

Add color settings to your config/settings_schema.json, one per role — the purpose a color serves — rather than one per element:

{
"type": "color",
"id": "color_body_text",
"label": {
"en-US": "Body text",
"zh-CN": "正文文字"
},
"default": "#292929"
}

Keep the set small. Roles like background, body text, headings, buttons, and borders cover most of a theme; when many elements share one role, merchants can restyle the store with a handful of pickers instead of hunting through dozens. Add element-specific colors only where merchants genuinely need independent control (for example, an announcement bar), and group them under clear headings in the settings sidebar.

Name the IDs predictably — a shared prefix such as color_ keeps them easy to find in Liquid and lets you process them in a loop (see below).

Normalize the values

The color picker stores values in two formats, and Liquid outputs them exactly as stored: #FFFFFF when the merchant picks an opaque color, and rgba(41, 41, 41, 1) when they touch the transparency input. Both are valid CSS, so if you only ever print the value into a stylesheet, you can use it as is.

Normalize when your CSS logic assumes one format — for example, when you derive variants from a base color. The color_to_hex filter converts any CSS color string to hex6:

{% assign color_body_text = settings.color_body_text | default: '#292929' %}
{% unless color_body_text contains '#' %}
{% assign color_body_text = color_body_text | color_to_hex %}
{% endunless %}

The default filter keeps the theme working before the merchant saves settings for the first time.

warning

color_to_hex drops the alpha component: rgba(41, 41, 41, 0.5) becomes #292929. Where the merchant's transparency choice matters — overlays, scrims, translucent headers — use the stored value directly instead of normalizing it.

Output the palette as CSS variables

Convert the settings into CSS variables once, in a snippet rendered from your layout, so the whole theme reads colors from a single place:

<style>
:root {
--color-body-text: {{ color_body_text }};
--color-heading-text: {{ color_heading_text }};
--color-btn-bg: {{ color_btn_bg }};
--color-btn-text: {{ color_btn_text }};
--color-border: {{ color_border }};
}
</style>

If your color settings share the color_ prefix, you can also generate the variables in a loop instead of listing each one:

{% for s in settings %}
{% if s[0] contains 'color_' %}
--{{ s[0] | replace: '_', '-' }}: {{ s[1] }};
{% endif %}
{% endfor %}

Consume the variables everywhere

In sections, snippets, and stylesheets, reference the variables instead of raw values:

.product-card__title {
color: var(--color-heading-text);
}
.btn--primary {
background: var(--color-btn-bg);
color: var(--color-btn-text);
}

When the merchant changes a color in the theme editor, every element wearing that role updates together. Hardcoded colors are acceptable only for elements whose contrast doesn't depend on merchant choices — anything a merchant can recolor should go through a variable.

Keep contrast accessible

A color system also concentrates your accessibility risk: one bad pairing of text and background colors spreads everywhere. Choose defaults that meet the contrast levels in accessibility best practices, and avoid pairing two merchant-configurable colors on elements where contrast is critical without sensible defaults.