# Creating Custom Themes with CSS Variables and Liquid Templates in PropertyWebBuilder

> Learn to create custom themes for PropertyWebBuilder using CSS variables and Liquid templates. Generate dynamic real estate websites with customizable styles and palettes.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: how-to-guide
- Published: 2026-03-01

---

**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](https://github.com/etewiah/property_web_builder) 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:

```erb
<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`](https://github.com/etewiah/property_web_builder/blob/main/app/helpers/pwb/css_helper.rb) (lines 32–40), dynamically selects a partial based on the active theme name:

```ruby
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:

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

```

The `generate_css_variables` method in [`app/services/pwb/palette_loader.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/palette_loader.rb) (lines 164–174) processes the JSON palette and produces CSS custom properties:

```ruby
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:

```css
: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`](https://github.com/etewiah/property_web_builder/blob/main/app/themes/mycity/palettes/standard.json):

   ```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`:

   ```erb
   <%= Pwb::PaletteLoader.new.generate_css_variables('mycity') %>
   ```

5. **Register the theme**  
   Insert a record using the `Theme` model ([`app/models/pwb/theme.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/theme.rb), lines 14–15):

   ```ruby
   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.

```liquid
<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`](https://github.com/etewiah/property_web_builder/blob/main/spec/helpers/pwb/css_helper_spec.rb)).

```ruby

# Override for a specific site

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

```

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

```liquid
<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`](https://github.com/etewiah/property_web_builder/blob/main/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`](https://github.com/etewiah/property_web_builder/blob/main/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.