Skip to main content

CSS Architecture

How CSS is organized and compiled in OQO themes.

Core Principle

Themes are self-contained and framework-agnostic.

You compile your CSS framework (Tailwind, Bootstrap, or custom) locally. OQO only handles settings-driven CSS variables and CSS bundling (@import resolution by inlining partials).

File Structure

your-theme/
├── assets/
│ ├── css/
│ │ ├── tailwind.css # Tailwind source (you compile this)
│ │ ├── _variables.css.liquid # CSS variables from theme settings
│ │ ├── _base.css # Base element styles
│ │ ├── _layout.css # Container/section layout utilities
│ │ ├── _typography.css # Text styling classes
│ │ ├── _components.css # Cards, buttons, etc. (.gz-card-*, .gz-btn-*)
│ │ ├── _sections.css # Section component styles (animations, hover effects)
│ │ ├── _utilities.css # Animations, images
│ │ └── theme.css # Entry point (@imports all partials)
│ ├── framework.css # Pre-compiled CSS framework output (DO NOT EDIT)
│ ├── theme.css # Final bundled output (auto-generated by OQO)
│ └── bundle.css # All CSS merged (auto-generated, for bundle mode)
├── sections/ # Section templates with {% style %} blocks
└── config/
└── settings_schema.json # Theme settings definition

How Compilation Works

On Theme Save

When you save theme settings, OQO automatically compiles your CSS:

  1. Load framework.css -- Your pre-compiled Tailwind/Bootstrap (if present)
  2. Render _variables.css.liquid -- Processed with Liquid using current settings
  3. Resolve @import statements -- theme.css entry point has its @import statements resolved by inlining partial files (plain CSS concatenation)
  4. Bundle output -- All parts concatenated into a single theme.css

Output Structure

theme.css (generated)
├── [framework.css] # Pre-compiled CSS framework
├── :root { ... } # CSS variables from settings
└── [resolved theme CSS] # Theme stylesheets with @imports inlined
tip

You never need to manually compile theme.css. OQO regenerates it automatically when theme files or settings change.

What Goes Where

I want to...Edit this fileThen run
Add a Tailwind utility class (mb-6, gap-4)Liquid templatetailwindcss ...
Add a custom component style (.gz-card-*)css/_components.cssNothing (auto)
Add section animation / hover effectscss/_sections.cssNothing (auto)
Change a theme variablecss/_variables.css.liquidNothing (auto)
Add section-specific CSS (colors, backgrounds){% style %} block in section .liquid fileNothing (auto)

Setting ID to CSS Variable Mapping

Theme settings automatically map to CSS variables with a --theme- prefix. Underscores in setting IDs become hyphens in CSS variable names:

Setting IDCSS Variable
primary_color--theme-primary-color
content_width--theme-content-width
border_radius--theme-border-radius

Define the mapping in _variables.css.liquid:

:root {
--theme-primary-color: {{ settings.primary_color | default: '#3B82F6' }};
--theme-content-width: {{ settings.content_width | default: 1280 }}px;
--theme-border-radius: {{ settings.border_radius | default: 8 }}px;
}

Section settings follow the same convention with a --section- prefix:

Setting IDCSS Variable
bg_color--section-bg-color
overlay_opacity--section-overlay-opacity

Per-Section CSS: The {% style %} Block

This is the core pattern for section-specific CSS. Each section template can include a {% style %}...{% endstyle %} block that defines CSS scoped to that section instance.

How It Works

  1. You write a {% style %} block in your section .liquid file
  2. OQO extracts it, renders the Liquid variables, and compiles it to a standalone CSS file
  3. Each section instance gets its own CSS file with a content-based hash for cache busting
  4. In the editor, these files hot-reload instantly when settings change -- no page refresh needed

Basic Example

sections/cta/cta_01.liquid
{%- comment -%} Template HTML {%- endcomment -%}
{% section_wrapper 'section' class: 'cta-01' %}
<div class="cta-01-content">
<h2 class="cta-01-title">{{ section.settings.title }}</h2>
<p class="cta-01-body">{{ section.settings.body }}</p>
</div>
{% endsection_wrapper %}

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
background-color: var(--section-bgcolor);

.cta-01-title { color: var(--section-heading); }
.cta-01-body { color: var(--section-text); }
}
{% endstyle %}

The Scoping Selector

Every {% style %} block must use this selector to scope CSS to the specific section instance:

[data-section-wrapper][data-section-id="{{ section.id }}"]

This targets the section wrapper element that {% section_wrapper %} generates automatically. The {{ section.id }} is a unique instance ID, so two instances of the same section type on a page each get their own CSS.

Compiled Output

OQO compiles each {% style %} block into a separate CSS file:

assets/sections/{category}-{name}-{instance_id}-{content_hash}.css

For example: cta-cta_01-S8vQP-jI-a1b2c3d4.css

The content hash changes when the CSS content changes, so browsers cache aggressively and only re-fetch when settings actually change.

What Belongs in {% style %} Blocks

Put in {% style %} blocks: CSS that changes with section settings -- variables, background colors, text colors, spacing driven by settings.

Put in _sections.css: Static CSS that never changes with settings -- animations, hover effects, transitions, perspective transforms. Scope these under the .gz-{category}-{name} class.

css/_sections.css
/* Static styles that don't change with settings */
.gz-banner-banner_05 {
.banner-05-image-wrap {
perspective: 1000px;
img {
transition: transform 0.4s ease, box-shadow 0.4s ease;
&:hover { transform: scale(1.03) rotateX(-2deg) rotateY(3deg); }
}
}
}
warning

Keep {% style %} blocks lightweight. They are re-rendered on every settings change. Animations, hover effects, and complex layouts should go in _sections.css instead.


Custom Colors Pattern

The custom_colors pattern lets site owners override theme colors on a per-section basis. It uses a checkbox toggle and color_alpha settings with {% if %} guards -- never | default: filters.

The Pattern

Schema settings:

{
"settings": [
{
"type": "checkbox",
"id": "custom_colors",
"label": "Custom Colors",
"info": "Override theme colors for this section"
},
{
"type": "color_alpha",
"id": "bgcolor",
"label": "Background Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "heading_color",
"label": "Heading Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "text_color",
"label": "Text Color",
"show_if": { "setting": "custom_colors", "eq": true }
}
]
}

Style block:

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
{% if section.settings.custom_colors and section.settings.bgcolor != blank %}
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
{% else %}
--section-bgcolor: var(--theme-background-color);
{% endif %}

{% if section.settings.custom_colors and section.settings.heading_color != blank %}
--section-heading: {{ section.settings.heading_color | color_alpha_to_rgba }};
{% else %}
--section-heading: var(--theme-heading-color);
{% endif %}

{% if section.settings.custom_colors and section.settings.text_color != blank %}
--section-text: {{ section.settings.text_color | color_alpha_to_rgba }};
{% else %}
--section-text: var(--theme-text-color);
{% endif %}

background-color: var(--section-bgcolor);

h2 { color: var(--section-heading); }
p { color: var(--section-text); }
}
{% endstyle %}

Key Rules

RuleWhy
Always check section.settings.custom_colors AND != blankThe checkbox must be enabled AND the color must be set
Use color_alpha type for all section colorsSupports both solid colors and colors with transparency
Always use color_alpha_to_rgba filterHandles both JSON {color, alpha} objects and plain hex strings
Fall back to var(--theme-*) variablesSections inherit theme colors by default
Never use `default:` for color fallbacks

Why Not | default:?

The | default: filter does not work reliably with color settings. An empty color input returns an empty string (''), which is truthy in Liquid -- so | default: never triggers. The explicit != blank check handles this correctly.

{%- comment -%} WRONG -- | default: never fires for empty strings {%- endcomment -%}
--section-bgcolor: {{ section.settings.bgcolor | default: 'var(--theme-background-color)' }};

{%- comment -%} CORRECT -- explicit blank check {%- endcomment -%}
{% if section.settings.custom_colors and section.settings.bgcolor != blank %}
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
{% else %}
--section-bgcolor: var(--theme-background-color);
{% endif %}

Section Wrapper Classes

The {% section_wrapper %} tag automatically adds standard classes and data attributes to every section:

{% section_wrapper 'section' class: 'my-custom-class' %}
<h2>My Section</h2>
{% endsection_wrapper %}

Renders:

<section class="gz-section gz-cta-cta_01 my-custom-class"
data-section-wrapper="true"
data-section-id="S8vQP-jI"
data-section-type="cta/cta_01">
<h2>My Section</h2>
</section>

Auto-Generated Attributes

AttributeExamplePurpose
gz-sectiongz-sectionCommon class shared by all sections (spacing, shared styles)
gz-{category}-{name}gz-cta-cta_01Section-type class for type-specific styles in _sections.css
data-section-wrappertrueMarker for the visual editor and CSS scoping
data-section-idS8vQP-jIUnique instance ID (used in {% style %} block selector)
data-section-typecta/cta_01Section template path

Use gz-section for styles shared across all sections (like vertical spacing). Use gz-{category}-{name} for type-specific component styles in _sections.css.


Edge-to-Edge Bleed Pattern

Sections can break out of their contained parent to extend to the full viewport width. This is useful for full-width banners, heroes, and navigation bars.

The CSS

.gz-section-bleed {
width: 100vw;
max-width: none;
margin-left: calc(-50vw + 50%);
}

How it works:

  • width: 100vw -- element expands to full viewport width
  • max-width: none -- removes any parent max-width constraints
  • margin-left: calc(-50vw + 50%) -- pulls element left to the viewport edge regardless of container width

Optional Bleed (Setting-Driven)

For sections where the user can toggle edge-to-edge:

sections/banner/banner_01.liquid
{%- assign section_class = 'relative' | append_if: section.settings.edge_to_edge, ' gz-section-bleed' -%}

{% section_wrapper 'div' class: section_class %}
<div class="gz-container">
<!-- Content stays contained within max-width -->
</div>
{% endsection_wrapper %}

With the schema setting:

{
"type": "checkbox",
"id": "edge_to_edge",
"label": "Edge to Edge",
"info": "Extend section to viewport edges"
}

Always-Bleed Sections

Navigation and footer sections typically always extend to viewport edges:

{% section_wrapper 'nav' class: 'gz-nav-bg py-4 px-4' %}
<div class="gz-container">
<!-- Navigation content -->
</div>
{% endsection_wrapper %}

Content Containment Inside Bleed Sections

When a section bleeds to viewport edges, wrap inner content in gz-container to keep it centered and contained:

{% section_wrapper 'div' class: 'gz-section-bleed' %}
<div class="gz-container">
<!-- Content is centered within max-width: var(--theme-content-width) -->
</div>
{% endsection_wrapper %}

Using Tailwind CSS

Source File

Create assets/css/tailwind.css:

@import "tailwindcss";

@source "../../sections/**/*.liquid";
@source "../../layouts/**/*.liquid";
@source "../../snippets/**/*.liquid";

The @source directives tell Tailwind to scan your Liquid files for class names.

Compile Locally

# In your theme directory
tailwindcss -i assets/css/tailwind.css -o assets/framework.css --watch

This produces a tree-shaken framework.css with only the utilities you actually use.

When to Recompile Tailwind

Run tailwindcss after:

  • Adding new Tailwind utility classes to Liquid templates (e.g., mb-6, gap-8)
  • Removing utility classes (to tree-shake unused CSS)
  • Modifying @source directives

You do NOT need to run Tailwind after:

  • Editing theme CSS files (OQO compiles these automatically)
  • Changing theme settings (variables auto-regenerate)
  • Editing Liquid template structure (only class name changes matter)

Using Theme Variables in Tailwind

Reference CSS variables in your Tailwind classes:

<div class="bg-[var(--theme-primary-color)]">
...
</div>

Minimal Setup (No Build Tools)

For simpler themes that don't need Tailwind, write CSS by hand. No build tools required.

  1. framework.css -- Contains all your styles as hand-written CSS using var(--theme-*) custom properties
  2. _variables.css.liquid -- Generates CSS variables from theme settings (only file processed by Liquid at compile time)
  3. theme.css (entry point) -- Minimal file that @imports any needed partials like fonts

This approach works well for focused themes with a small number of custom styles.


CSS Loading: The {% theme_css %} Tag

CSS is loaded via the {% theme_css %} tag in your theme layout:

layouts/theme.liquid
<head>
{% theme_css %}
{% theme_asset 'theme.js' %}
</head>

Output Modes

UsageModeOutput
{% theme_css %}Defaulttheme.css + individual section CSS files
{% theme_css bundle: true %}BundleSingle bundle.css file (all CSS merged)
(editor preview)Previewtheme.css + hot-reloadable section CSS links

Default mode loads theme.css plus separate CSS files for each section on the page. This allows browser caching of section CSS across pages.

Bundle mode loads a single bundle.css file containing all CSS. Use this for production when you want a single HTTP request.

Preview mode (automatic in the theme editor) loads individual section CSS files with cache-busting timestamps for instant hot-reload.

Default Mode Output Example

<link rel="stylesheet" href="/themes/default/assets/theme-abc123.css">
<link rel="stylesheet" href="/themes/default/sections/banner-banner_03-id-hash.css">
<link rel="stylesheet" href="/themes/default/sections/cta-cta_01-id-hash.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@4.5.0/fonts/remixicon.css">

Developer Guidelines

DO

PracticeWhy
Use {% style %} blocks for section CSS variables and simple rulesEnables hot-reload and surgical cache invalidation
Scope with [data-section-wrapper][data-section-id="{{ section.id }}"]Targets the correct section instance
Put animations, hover effects, complex CSS in _sections.cssStatic styles scoped under .gz-{category}-{name}
Use --section-* prefix for section CSS variablesDistinguishes from --theme-* global variables
Use hyphens in CSS variable namesConvention: --section-bg-color not --section-bg_color
Use color_alpha_to_rgba filter for all section color settingsHandles both {color, alpha} JSON and plain hex strings
Use image_url filter for image URLsGenerates proper variant URLs with CDN support
Use gz-container inside bleed sectionsKeeps content centered within the theme's max-width

DON'T

Anti-PatternWhy It Breaks
Use inline style: attributes for settings-driven CSSCannot be hot-reloaded; requires full HTML re-render
Put animations or hover effects in {% style %} blocksRe-rendered on every settings change; must be lightweight
Hardcode colors in section CSSSettings and live preview will not work
Use underscores in CSS variable namesConvention violation (setting IDs use underscores, CSS vars use hyphens)
Edit generated files (theme.css, framework.css, bundle.css)Overwritten on next compilation
Reset margins/padding in unlayered theme CSSBreaks Tailwind utility classes (theme CSS has higher cascade priority)
Use `default:` for color fallbacks

Where to Edit -- Quick Reference

I want to...Edit this file
Add section CSS variables (colors, backgrounds){% style %} block in sections/{category}/{name}.liquid
Add section component styles (animations, hover)css/_sections.css (under .gz-{category}-{name})
Add a theme-level CSS variablecss/_variables.css.liquid
Add a custom component stylecss/_components.css
Add animation utilitiescss/_utilities.css
Add a Tailwind utility classLiquid template, then run tailwindcss

Complete Section Example

Here is a full section template showing the {% style %} block, custom_colors pattern, and schema together:

sections/cta/cta_03.liquid
{% section_wrapper 'section' class: 'cta-03' %}
<div class="cta-03-content gz-container">
<h2 class="cta-03-title">{{ section.settings.title }}</h2>
<div class="cta-03-body">{{ section.settings.body }}</div>
<a href="{{ section.settings.button.href }}" class="cta-03-button">
{{ section.settings.button.text }}
</a>
</div>
{% endsection_wrapper %}

{% style %}
[data-section-wrapper][data-section-id="{{ section.id }}"] {
{% if section.settings.custom_colors and section.settings.bgcolor != blank %}
--section-bgcolor: {{ section.settings.bgcolor | color_alpha_to_rgba }};
{% else %}
--section-bgcolor: var(--theme-background-color);
{% endif %}
{% if section.settings.custom_colors and section.settings.heading_color != blank %}
--section-heading: {{ section.settings.heading_color | color_alpha_to_rgba }};
{% else %}
--section-heading: var(--theme-heading-color);
{% endif %}
{% if section.settings.custom_colors and section.settings.text_color != blank %}
--section-text: {{ section.settings.text_color | color_alpha_to_rgba }};
{% else %}
--section-text: var(--theme-text-color);
{% endif %}

background-color: var(--section-bgcolor);

.cta-03-title { color: var(--section-heading); }
.cta-03-body { color: var(--section-text); }
.cta-03-button {
background-color: var(--section-heading);
color: var(--section-bgcolor);
}
}
{% endstyle %}

{% schema %}
{
"name": "Call to Action",
"category": "cta",
"settings": [
{ "type": "header", "content": "Colors" },
{
"type": "checkbox",
"id": "custom_colors",
"label": "Custom Colors",
"info": "Override theme colors for this section"
},
{
"type": "color_alpha",
"id": "bgcolor",
"label": "Background Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "heading_color",
"label": "Heading Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{
"type": "color_alpha",
"id": "text_color",
"label": "Text Color",
"show_if": { "setting": "custom_colors", "eq": true }
},
{ "type": "header", "content": "Content" },
{ "type": "text", "id": "title", "label": "Title" },
{ "type": "text", "id": "body", "label": "Body", "html": true },
{ "type": "link", "id": "button", "label": "Button", "with_text": true }
]
}
{% endschema %}

This example demonstrates:

  • The {% section_wrapper %} tag generating gz-section and gz-cta-cta_03 classes
  • The {% style %} block with the [data-section-wrapper][data-section-id] scoping selector
  • The custom_colors checkbox pattern with != blank guards
  • color_alpha settings with the color_alpha_to_rgba filter
  • Fallback to --theme-* variables when custom colors are not set