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:
-
Create the theme directory
Create a folder underapp/themes/mycity/with aviews/subdirectory that mirrors the default theme structure. -
Add Liquid view templates
Copy existing templates likepwb/welcome/index.html.erbandpwb/_header.html.erbintoapp/themes/mycity/views/and customize the markup. -
Define a color palette
Create a JSON file atapp/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" } } } -
Create the CSS partial
Addapp/views/pwb/custom_css/_mycity.css.erb:<%= Pwb::PaletteLoader.new.generate_css_variables('mycity') %> -
Register the theme
Insert a record using theThememodel (app/models/pwb/theme.rb, lines 14–15):Pwb::Theme.create!(name: 'mycity', friendly_name: 'MyCity Theme') -
Activate the theme
Set thetheme_namecolumn on the website to"mycity"via the admin UI attenant_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_variablesthrough thecustom_styleshelper into the layout's<style>block. - Variable naming follows the
--pwb-prefix convention and attaches to:rootfor global availability. - Registration happens via the
Pwb::Thememodel, linking folder names to database records for admin selection. - Overrides can be applied per-site using the
style_variablesdatabase 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →