Creating Custom Themes with CSS Variables and Liquid Templates in PropertyWebBuilder

PropertyWebBuilder generates themeable real estate websites by combining Liquid templates with JSON-defined color palettes that compile to CSS custom properties injected at runtime.

Creating custom themes with CSS variables and Liquid templates in the PropertyWebBuilder open-source platform requires placing files in specific theme directories and registering the theme in the database. Every public site renders from a theme folder located under app/themes/, where Liquid views, palette definitions, and CSS partials work together to produce a cohesive design system.

How Theme CSS Generation Works

The rendering pipeline relies on a layout-based injection system that converts JSON color definitions into CSS custom properties.

The Layout Injection Points

Each theme provides a base layout at app/themes/<theme_name>/views/layouts/pwb/application.html.erb that contains a critical <style> block. According to the source code in the Brisbane theme example, this block calls four helpers:

<style>
  <%= critical_css %>
  <%= palette_css %>
  <%= font_css_variables %>
  <%= custom_styles "#{@current_website.theme_name}" %>
</style>
  • critical_css – Inlines essential Tailwind utility classes.
  • palette_css – Loads pre-compiled or dynamically generated color rules.
  • font_css_variables – Injects CSS variables for selected Google Fonts.
  • custom_styles – Renders the theme-specific CSS partial that exposes palette variables to :root.

The custom_styles helper, defined in app/helpers/pwb/css_helper.rb (lines 32–40), dynamically selects a partial based on the active theme name:

def custom_styles(theme_name)
  render partial: "pwb/custom_css/#{theme_name}", formats: :css
end

Palette Loading and CSS Variable Generation

When the partial renders—such as app/views/pwb/custom_css/_brisbane.css.erb—it invokes Pwb::PaletteLoader to generate the actual CSS:

<%= Pwb::PaletteLoader.new.generate_css_variables('brisbane', @current_website.palette_id) %>

The generate_css_variables method in app/services/pwb/palette_loader.rb (lines 164–174) processes the JSON palette and produces CSS custom properties:

def generate_css_variables(theme_name, palette_id = nil, include_dark_mode: false)
  palette = palette_id ? get_palette(theme_name, palette_id) : get_default_palette(theme_name)
  return "" unless palette
  if include_dark_mode
    light_colors = extract_light_colors(palette)
    dark_colors  = has_explicit_dark_mode?(palette) ? palette.dig("modes","dark") : nil
    ColorUtils.generate_dual_mode_css_variables(light_colors, dark_colors)
  else
    ColorUtils.generate_palette_css_variables(palette)
  end
end

The resulting CSS attaches variables to :root, making them globally available:

:root {
  --pwb-primary-color: #1e3a8a;
  --pwb-primary-color-light: #3b82f6;
  --pwb-primary-color-dark: #1e40af;
  --pwb-accent-color: #f59e0b;
}
@media (prefers-color-scheme: dark) {
  :root {
    --pwb-primary-color: #93c5fd;
  }
}

Step-by-Step Guide to Creating a Custom Theme

Follow these steps to create a fully functional theme named mycity:

  1. Create the theme directory
    Create a folder under app/themes/mycity/ with a views/ subdirectory that mirrors the default theme structure.

  2. Add Liquid view templates
    Copy existing templates like pwb/welcome/index.html.erb and pwb/_header.html.erb into app/themes/mycity/views/ and customize the markup.

  3. Define a color palette
    Create a JSON file at app/themes/mycity/palettes/standard.json:

    {
      "name": "Standard",
      "description": "Base palette for MyCity theme",
      "preview_colors": ["#1e3a8a", "#f59e0b", "#10b981"],
      "is_default": true,
      "supports_dark_mode": true,
      "primary": "#1e3a8a",
      "primary-light": "#3b82f6",
      "primary-dark": "#1e40af",
      "accent": "#f59e0b",
      "background": "#ffffff",
      "text": "#111827",
      "modes": {
        "dark": {
          "primary": "#93c5fd",
          "background": "#111827",
          "text": "#f9fafb"
        }
      }
    }
  4. Create the CSS partial
    Add app/views/pwb/custom_css/_mycity.css.erb:

    <%= Pwb::PaletteLoader.new.generate_css_variables('mycity') %>
  5. Register the theme
    Insert a record using the Theme model (app/models/pwb/theme.rb, lines 14–15):

    Pwb::Theme.create!(name: 'mycity', friendly_name: 'MyCity Theme')
  6. Activate the theme
    Set the theme_name column on the website to "mycity" via the admin UI at tenant_admin/websites/appearance_form.html.erb (lines 63–66).

Using CSS Variables in Liquid Templates

Because variables are scoped to :root, any Liquid template can reference them directly using standard CSS var() syntax. All PropertyWebBuilder theme variables use the --pwb- prefix.

<style>
  .hero {
    background: var(--pwb-primary-color);
    color: var(--pwb-primary-color-text);
  }
  .btn-primary {
    background: var(--pwb-accent-color);
    border-color: var(--pwb-primary-color-dark);
  }
</style>

<div class="hero">
  <h1>{{ page.title }}</h1>
  <a class="btn-primary" href="{{ contact_path }}">Contact us</a>
</div>

The variables cascade automatically without additional Ruby processing, keeping presentation logic decoupled from backend code.

Extending Themes with Site-Specific Overrides

For one-off brand customizations without modifying the theme files, use the style_variables column on the websites table. The Website#style_variables_for_theme method merges these values with the generated palette, and the custom_styles helper renders the merged map (verified in spec/helpers/pwb/css_helper_spec.rb).


# Override for a specific site

website.update!(style_variables: { "primary-color" => "#ff6600" })

The overridden value is injected into :root alongside standard palette variables:

<div style="background: var(--pwb-primary-color);">
  Custom colour for this site only
</div>

Summary

  • Theme structure requires directories under app/themes/<name>/ containing Liquid views and palette JSON files.
  • CSS generation flows from Pwb::PaletteLoader#generate_css_variables through the custom_styles helper into the layout's <style> block.
  • Variable naming follows the --pwb- prefix convention and attaches to :root for global availability.
  • Registration happens via the Pwb::Theme model, linking folder names to database records for admin selection.
  • Overrides can be applied per-site using the style_variables database column without touching theme files.

Frequently Asked Questions

Where are theme files stored in PropertyWebBuilder?

Theme files reside under app/themes/<theme_name>/ within the Rails application. This directory contains a views/ folder for Liquid templates (.erb files) and a palettes/ folder for JSON color definitions. The Theme model in app/models/pwb/theme.rb references these folders by name.

How does PropertyWebBuilder convert JSON palettes to CSS?

The Pwb::PaletteLoader service reads JSON files from app/themes/<theme>/palettes/ and converts color keys into CSS custom properties. The generate_css_variables method processes light and dark mode values, then ColorUtils formats them into CSS variable declarations attached to the :root selector.

Can I override theme colors for a single website without creating a new theme?

Yes. Add a hash to the style_variables column on the specific website record. The Website#style_variables_for_theme method merges these values with the theme's default palette, and the custom_styles helper injects them into the page. This allows per-tenant branding while sharing the same underlying theme templates.

What is the role of the custom_styles helper?

The custom_styles helper, defined in app/helpers/pwb/css_helper.rb, renders a CSS partial named after the active theme (e.g., _brisbane.css.erb). This partial invokes the palette loader to generate CSS variables, ensuring that the correct color scheme loads for the current website's selected theme and palette ID.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →