Last updated

Linked CSS Variable Presets

Several inputs in the page builder support linked CSS variable presets — instead of hardcoding values like font-size: 32px or border-radius: 8px, the builder inserts CSS variable references. This keeps content in sync with theme settings: when a company changes their heading size, brand color, spacing, or corner radius, all linked values update automatically.

How values are stored: When a linked preset is used in the page builder, the persisted value is the full var() form — for example border-radius: var(--corner_radius_md) or color: var(--color_primary). It is not stored as a bare custom property token like --corner_radius_md alone (that would not be valid in a CSS property value). In theme.liquid you still declare properties as --name: …; the storefront and saved builder content reference them as var(--name).

Linked presets are available in:

  • Rich text editor — text size, font family, font weight, and color presets
  • Corner radius inputs — corner radius presets
  • Padding inputs — spacing/padding presets

Table of Contents


How It Works

When a company selects a linked preset in the rich text editor, the output uses CSS variable references wrapped in var(). Size, font family, and font weight each link independently, so any of them can be a var():

<span style="font-size: var(--font_size_h1); font-family: var(--font_family_heading); font-weight: var(--font_weight_heading);">
  Welcome to our store
</span>
<span style="color: var(--color_primary);">
  Shop now
</span>

If the company unlinks and switches to manual controls, the resolved values are applied directly instead:

<span style="font-size: 32px; font-family: Montserrat; font-weight: bold;">
  Welcome to our store
</span>
<span style="color: #2563eb;">
  Shop now
</span>

For this to work, your theme needs two things: settings defined in settings_schema.json and CSS variables wired in theme.liquid.


Setup

Step 1: Define Settings in settings_schema.json

The builder looks for settings in specific groups by name. Settings in these groups become available as presets:

Group NameWhat It Powers
typographyFont size presets in the rich text editor
color_schemaColor presets in the rich text editor
corner_radiusCorner radius presets
paddingPadding/spacing presets
custom_font_sizesAdditional custom text size presets (optional)
custom_colorsAdditional custom color presets (optional)

Font family and font weight are resolved per text-size preset (see How Text Size Presets Are Built). A preset can link them in two ways: by rolefont_family_heading / font_family_body are picked up by setting ID from any group — or explicitly, via font_family_ref / font_weight_ref pointing at any setting ID. Font-family pickers and weight settings are only read as sources; they are never turned into presets themselves, so they can live anywhere (a separate fonts group keeps things tidy).

Example typography and font settings:

The font_family_ref / font_weight_ref fields below link each size preset's family and weight to a specific setting (and its CSS variable). Omit them to fall back to the role-derived font_family_heading / font_family_body and the bold / normal defaults.

[
  {
    "name": "fonts",
    "settings": [
      {
        "type": "font_picker",
        "id": "font_family_heading",
        "label": "Heading Font",
        "default": "Montserrat"
      },
      {
        "type": "font_picker",
        "id": "font_family_body",
        "label": "Body Font",
        "default": "Inter"
      },
      {
        "type": "range",
        "id": "font_weight_heading",
        "label": "Heading Weight",
        "min": 100,
        "max": 900,
        "step": 100,
        "default": 700
      },
      {
        "type": "range",
        "id": "font_weight_body",
        "label": "Body Weight",
        "min": 100,
        "max": 900,
        "step": 100,
        "default": 400
      }
    ]
  },
  {
    "name": "typography",
    "settings": [
      {
        "type": "range",
        "id": "font_size_h1",
        "label": "Heading 1 Size",
        "min": 16,
        "max": 80,
        "step": 1,
        "default": 40,
        "unit": "px",
        "role": "heading",
        "font_family_ref": "font_family_heading",
        "font_weight_ref": "font_weight_heading"
      },
      {
        "type": "range",
        "id": "font_size_body",
        "label": "Body Size",
        "min": 12,
        "max": 32,
        "step": 1,
        "default": 16,
        "unit": "px",
        "role": "body",
        "font_family_ref": "font_family_body",
        "font_weight_ref": "font_weight_body"
      }
    ]
  }
]

Example color settings:

{
  "name": "color_schema",
  "settings": [
    {
      "type": "color",
      "id": "color_primary",
      "label": "Primary Color",
      "default": "#2563eb"
    },
    {
      "type": "color",
      "id": "color_body",
      "label": "Body Text Color",
      "default": "#1a1a2e"
    }
  ]
}

Step 2: Wire Settings to CSS Variables in theme.liquid

Inside a {%- style -%}...{%- endstyle -%} or <style>...</style> block in layouts/theme.liquid, define CSS custom properties that reference your settings:

{'%' style '%'}
  :root {
    /* Typography */
    --font_size_h1: {{ settings.font_size_h1 | append: 'px' }};
    --font_size_h2: {{ settings.font_size_h2 | append: 'px' }};
    --font_size_h3: {{ settings.font_size_h3 | append: 'px' }};
    --font_family_heading: {{ settings.font_family_heading | font_family }};
    --font_family_body: {{ settings.font_family_body | font_family }};
    --font_weight_heading: {{ settings.font_weight_heading }};
    --font_weight_body: {{ settings.font_weight_body }};

    /* Colors */
    --color_primary: {{ settings.color_primary }};
    --color_body: {{ settings.color_body }};
    --color_heading: {{ settings.color_heading }};

    /* Corner Radius */
    --corner_radius_sm: {{ settings.corner_radius_sm | append: 'px' }};
    --corner_radius_md: {{ settings.corner_radius_md | append: 'px' }};
    --corner_radius_lg: {{ settings.corner_radius_lg | append: 'px' }};

    /* Padding / Spacing */
    --spacing_sm: {{ settings.spacing_sm | append: 'px' }};
    --spacing_md: {{ settings.spacing_md | append: 'px' }};
    --spacing_lg: {{ settings.spacing_lg | append: 'px' }};
  }
{'%' endstyle '%'}

If a setting in a recognized group has a matching CSS variable in theme.liquid, the preset becomes linkable in the builder.


Responsive Values

Because linked presets resolve to var() references, the value a preset renders is whatever the CSS variable holds at that point in the cascade. Redefine the variable inside a media query and every linked value updates at that breakpoint automatically — the saved builder content does not change, because it still reads var(--token).

Declare a base value in :root and override it inside a @media block in theme.liquid:

:root {
  --gap: 12px;
}

@media (min-width: 768px) {
  :root {
    --gap: 24px;
  }
}

Any content linked to var(--gap) renders 12px below 768px and 24px at or above it. The same approach works for any wired token — typography, color, corner radius, or padding — so a single linked preset adapts across breakpoints from one declaration in theme.liquid.


How Text Size Presets Are Built

Every size setting in the typography group becomes one entry in the rich text editor's Text Presets dropdown. Each preset has three dimensions — size, font family, and font weight — and each dimension is resolved (and linked to a CSS variable) independently.

Only size settings become presets. Settings typed font_picker / font_family / font (font-family pickers) and font_weight are skipped when building presets — they are read as sources for a preset's family/weight, never turned into presets themselves. A font picker sitting in the typography group is therefore harmless and does not create a phantom preset. Grouping pickers and weights under a separate fonts group (as in the example above) is still the clearest layout.

Resolving each dimension

For every size setting, the builder resolves family, weight, and size using this precedence:

Font family — explicit ref → per-setting literal → role-derived theme font:

  1. font_family_ref → resolves from the referenced setting and links to that setting's CSS variable (e.g. var(--font_family_heading)).
  2. font_family (a literal value on the setting) → applied as a concrete value, not linked.
  3. Otherwise the role decides: a heading uses font_family_heading, body uses font_family_body (linked to their vars when present). Heading and body fall back to each other if only one is defined; if neither exists the picker shows "Inherited" and no family is applied (text inherits the surrounding theme CSS).

Font weight — explicit ref → per-setting literal → role default:

  1. font_weight_ref → resolves from the referenced setting and links to its CSS variable (e.g. var(--font_weight_heading)). The referenced setting is resolved by ID regardless of its type, so a weight authored as a range works.
  2. font_weight (a literal) → applied as a concrete value.
  3. Otherwise the role default: bold for headings, normal for body.

Font size — the setting's own value, suffixed with its unit, linked to the setting's CSS variable when one exists.

How role is determined

A setting's role (heading vs body) drives the default family and weight:

  • An explicit role field ("heading" or "body") on the setting takes precedence.
  • Otherwise the builder falls back to the legacy heuristic: an ID that starts with font_size_h is a heading (a loose prefix match — font_size_hero / font_size_header also count), everything else is body.

Prefer the explicit role field for new themes; the font_size_h* prefix is only a fallback so existing themes keep working.

Value precedence

Each dimension's value resolves through this chain:

live builder value (unsaved edits in the Theme panel) → saved value (the merchant's current setting value) → schema defaulthardcoded fallback (16px for size, #000000 for color)

In the builder, the preset list reflects in-flight Theme-panel edits immediately, so the dropdown matches the live preview before you save. Saved storefront content uses the saved value.

Units

The size is emitted with the setting's unit (defaulting to px when omitted), and decimal values are preserved — so rem/em sizes such as 2.5rem work. (min / max / step are not part of the resolved value; they only constrain the slider.)


Corner Radius Presets

Corner radius inputs support linked CSS variable presets. Define corner radius settings in the corner_radius group and map them to CSS variables in theme.liquid.

settings_schema.json:

{
  "name": "corner_radius",
  "settings": [
    {
      "type": "range",
      "id": "corner_radius_sm",
      "label": "Small",
      "min": 0,
      "max": 24,
      "step": 1,
      "default": 4,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "corner_radius_md",
      "label": "Medium",
      "min": 0,
      "max": 32,
      "step": 1,
      "default": 8,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "corner_radius_lg",
      "label": "Large",
      "min": 0,
      "max": 48,
      "step": 1,
      "default": 16,
      "unit": "px"
    }
  ]
}

When a preset is selected, the saved value is the var() reference (e.g. var(--corner_radius_md)), not the bare name --corner_radius_md. The input shows the preset label in read-only mode. When corners are linked, the preset applies to all four corners.


Padding Presets

Padding inputs support the same linked preset pattern using the padding group name.

settings_schema.json:

{
  "name": "padding",
  "settings": [
    {
      "type": "range",
      "id": "spacing_sm",
      "label": "Small",
      "min": 0,
      "max": 48,
      "step": 1,
      "default": 8,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "spacing_md",
      "label": "Medium",
      "min": 0,
      "max": 64,
      "step": 1,
      "default": 16,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "spacing_lg",
      "label": "Large",
      "min": 0,
      "max": 96,
      "step": 1,
      "default": 32,
      "unit": "px"
    }
  ]
}

When a preset is selected, the saved value uses var(--token) on the selected side(s) (for example padding-top: var(--spacing_md)), not bare --spacing_md. When sides are linked, the preset applies to all four sides.


How the Mapping Works

The admin parses theme.liquid and matches this pattern:

--{css_var_name}: {{ settings.{setting_id} ...

It builds a map of setting_id to css_var_name (the --… name from the left-hand side of the declaration). When the builder saves a linked preset, it writes that token inside var() in the stored style (e.g. mapping color_primary--color_primary results in color: var(--color_primary) in saved content).

Line in theme.liquidDetected Mapping
--font_size_h1: {{ settings.font_size_h1 | append: 'px' }};font_size_h1--font_size_h1
--color_primary: {{ settings.color_primary }};color_primary--color_primary
--font_family_heading: {{ settings.font_family_heading | font_family }};font_family_heading--font_family_heading
--font_weight_heading: {{ settings.font_weight_heading }};font_weight_heading--font_weight_heading

Naming Convention

The CSS variable name does not need to match the setting ID. All of these work:

--font_size_h1: {{ settings.font_size_h1 | append: 'px' }};
--heading-size-1: {{ settings.font_size_h1 | append: 'px' }};
--h1-size: {{ settings.font_size_h1 | append: 'px' }};

The parser matches on the settings.{id} part, not the variable name. However, keeping names consistent (e.g., --font_size_h1 for settings.font_size_h1) is recommended for clarity.


What Gets Linked and What Doesn't

Not everything becomes a linked preset:

  • Custom presets (from custom_font_sizes / custom_colors groups) — when you add a custom preset in the builder it is linked, not baked in: the builder injects a --<id>: {{ settings.<id> }} declaration into theme.liquid and the preset resolves to that variable, exactly like a built-in preset. A custom text preset carries its font_family and font_weight as concrete literals (it does not use the heading/body role), so it links its size to a variable while applying its family and weight as fixed values.
  • Settings without a CSS variable in theme.liquid — if a setting exists in settings_schema.json but has no corresponding --var: {{ settings.id }} line in theme.liquid, it will still appear as a preset but will apply the resolved value directly.
  • Font weight — linkable. A weight resolved from a font_weight_ref links to that setting's CSS variable (e.g. var(--font_weight_heading)); a weight from a literal font_weight or the role default (bold / normal) is applied as a concrete value.

Default Behavior (Linked Presets)

The toolbar shows two preset pickers: Text Presets and Color Presets. Each preset in the dropdown corresponds to a theme setting. Selecting a preset writes inline styles using var(--setting_token) (always the var() wrapper), matching the rich text, corner radius, and padding behavior above.

Unlinking

Each preset picker has an "Unlink Variables" option in its footer. Clicking it:

  1. Resolves any active CSS variable values back to their current concrete values
  2. Switches the toolbar to manual mode showing individual controls: Font Family picker, Font Size picker, and Color picker

The toolbar has a "Link Variables" option to switch back to preset mode.