Creating Website Themes
This guide explains how to create custom themes for the Fullfinity Website Builder module.
Overview
Section titled “Overview”A theme in Fullfinity is a standalone module that provides:
- Theme Configuration - CSS variables, fonts, settings schema, color presets
- Section Templates - Jinja2 templates that override base templates with theme-specific markup
- Static Assets - CSS, JavaScript, and images
Themes are auto-discovered when:
- Module has
category: module_category_themesinmanifest.yaml - Website module is installed
Theme Architecture
Section titled “Theme Architecture”The website module provides BASE templates that are pure Bootstrap 5 with minimal styling.
Themes provide the visual experience by:
- Section Template Overrides (
templates/section_templates.json) - Enhanced markup with theme-specific classes - CSS Styling (
static/css/main.css) - Fonts, colors, animations, visual enhancements - JavaScript (
static/js/main.js) - Interactive effects (Preact-based)
Key Principle: Templates are looked up by identifier at render time. When a theme provides a template with the same identifier as the base website module, the theme’s template is used.
Theme Module Structure
Section titled “Theme Module Structure”modules/website_theme_mytheme/├── manifest.yaml # Module metadata (category = Themes)├── __init__.py # Optional hooks (post_install, etc.)├── views/│ └── theme.yaml # WebsiteTheme record├── templates/│ ├── section_templates.yaml # Section template definitions│ ├── hero_centered.jinja # Hero section template│ ├── features_grid.jinja # Features section template│ ├── footer.jinja # Footer template│ └── ... # Other section templates└── static/ ├── css/main.css # Theme styles ├── js/main.js # Theme JavaScript (Preact) └── images/ └── thumbnail.png # Theme preview imageCreating a Theme
Section titled “Creating a Theme”1. Create the Module Structure
Section titled “1. Create the Module Structure”mkdir -p modules/website_theme_mytheme/{views,templates,static/{css,js,images}}2. manifest.yaml
Section titled “2. manifest.yaml”name: My Custom Themeidentifier: website_theme_mythemeversion: '1.0'category: module_category_themesdescription: A beautiful custom theme for Fullfinity websitesdependencies:- websiteicon: Paletteimage: /static/images/thumbnail.pngstatic_paths:- css- js- imagesImportant: The category must be "module_category_themes" for auto-discovery.
3. Theme Definition (views/theme.yaml)
Section titled “3. Theme Definition (views/theme.yaml)”[ { "data_type": "WebsiteTheme", "identifier": "website_theme_mytheme", "name": "My Custom Theme", "description": "A beautiful custom theme", "theme_module": "website_theme_mytheme", "thumbnail": "../static/images/thumbnail.png", "css_files": ["css/main.css"], "js_files": ["js/main.js"], "css_variables": { "--bs-primary": "#4f46e5", "--bs-secondary": "#7c3aed", "--theme-primary": "#4f46e5", "--theme-secondary": "#7c3aed", "--theme-accent": "#06b6d4", "--theme-text": "#1f2937", "--theme-text-muted": "#6b7280", "--theme-border": "#e5e7eb", "--theme-footer-bg": "#111827", "--theme-footer-text": "#d1d5db" }, "settings_schema": [...], "presets": [...] }]Theme Fields
Section titled “Theme Fields”| Field | Description |
|---|---|
identifier | Unique identifier (should match module identifier) |
name | Display name |
description | Theme description |
theme_module | Module identifier (for asset path resolution) |
thumbnail | Path to preview image |
css_files | Array of CSS files (relative to module’s static folder) |
js_files | Array of JS files (relative to module’s static folder) |
css_variables | CSS custom properties injected into pages |
settings_schema | Schema for theme customization UI |
presets | Predefined color combinations |
4. CSS Variables
Section titled “4. CSS Variables”CSS variables from css_variables are injected into the page’s <style> tag. Use both Bootstrap variables (--bs-*) and theme-specific variables (--theme-*).
Every theme MUST define the complete standard variable set. Other modules (ecommerce, blog, etc.) rely on these variables to style their pages. If a theme omits any variable, those modules will fall back to hardcoded defaults that may not match the theme’s design.
Standard Variable Reference
Section titled “Standard Variable Reference”All themes must provide every variable listed below in their :root block:
| Category | Variable | Description |
|---|---|---|
| Typography | --theme-heading_font | Heading font family |
--theme-body_font | Body font family | |
| Colors | --theme-primary | Primary brand color |
--theme-secondary | Secondary/accent color | |
--theme-accent | Highlight/focus color | |
--theme-success | Success state | |
--theme-warning | Warning state | |
--theme-danger | Error/danger state | |
| Text | --theme-text | Primary text color |
--theme-text_muted | Secondary/muted text | |
--theme-heading_color | Heading text color | |
--theme-subheading_color | Subheading text color | |
| Backgrounds | --theme-background | Page background |
--theme-surface | Card/panel background | |
--theme-surface-hover | Surface hover state | |
--theme-border | Default border color | |
--theme-border-hover | Border hover state | |
| Navbar | --theme-navbar_bg | Navbar background |
--theme-navbar_text | Navbar text color | |
| Footer | --theme-footer_bg | Footer background |
--theme-footer_text | Footer text color | |
--theme-footer_text_muted | Footer muted text | |
| Gradients | --theme-gradient | Primary gradient |
--theme-gradient-subtle | Subtle/light gradient | |
--theme-primary-hover | Primary color hover state | |
--theme-secondary-hover | Secondary color hover state | |
| Shadows | --theme-shadow-xs | Extra small shadow |
--theme-shadow-sm | Small shadow | |
--theme-shadow | Default shadow | |
--theme-shadow-md | Medium shadow | |
--theme-shadow-lg | Large shadow | |
--theme-shadow-colored | Brand-colored shadow | |
| Border Radius | --theme-radius-sm | Small (4px) |
--theme-radius | Default (6px) | |
--theme-radius-lg | Large (8px) | |
--theme-radius-xl | Extra large (12px) | |
--theme-radius-2xl | 2x large (16px) | |
--theme-radius-full | Pill shape (100px) | |
| Transitions | --theme-transition-fast | Fast (0.1s) |
--theme-transition | Default (0.15s) | |
--theme-transition-slow | Slow (0.25s) | |
--theme-transition-smooth | Smooth easing (0.3s) | |
| Buttons | --theme-btn-primary-bg | Primary button background |
--theme-btn-primary-text | Primary button text | |
--theme-btn-primary-border | Primary button border | |
--theme-btn-primary-hover-bg | Primary button hover | |
--theme-btn-secondary-bg | Secondary button background | |
--theme-btn-secondary-text | Secondary button text | |
--theme-btn-secondary-border | Secondary button border | |
--theme-btn-secondary-hover-bg | Secondary button hover | |
| Utility | --theme-primary-foreground | Text on primary backgrounds |
--theme-icon-filter | CSS filter for icons (e.g. invert(1) for dark mode) | |
--theme-login-logo-filter | CSS filter for login page logo |
Using Theme Variables in Module CSS
Section titled “Using Theme Variables in Module CSS”Module CSS should use --theme-* variables directly with raw fallbacks:
.my-card { background: var(--theme-surface, #fafafa); border: 1px solid var(--theme-border, #e5e7eb); border-radius: var(--theme-radius-lg, 0.75rem); color: var(--theme-text, #1f2937);}The fallback values ensure the module works even without a theme installed. When a theme is active, its variables override the fallbacks automatically.
Portal Integration
Section titled “Portal Integration”The customer portal automatically inherits your theme’s CSS variables. No extra configuration needed - the portal will match your theme out of the box.
5. Settings Schema
Section titled “5. Settings Schema”Define customizable settings for the theme editor. Settings are grouped into sections:
"settings_schema": [ { "name": "Colors", "settings": [ { "type": "header", "content": "Primary Colors" }, { "type": "color", "id": "primary", "label": "Primary", "default": "#4f46e5", "info": "Buttons, links" }, { "type": "color", "id": "secondary", "label": "Secondary", "default": "#7c3aed" } ] }, { "name": "Typography", "settings": [ { "type": "font", "id": "heading", "label": "Heading Font", "default": "Inter, system-ui, sans-serif" }, { "type": "font", "id": "body", "label": "Body Font", "default": "Inter, system-ui, sans-serif" } ] }, { "name": "Layout", "settings": [ { "type": "select", "id": "border_radius", "label": "Border Radius", "options": [ { "value": "none", "label": "None" }, { "value": "small", "label": "Small" }, { "value": "medium", "label": "Medium" }, { "value": "large", "label": "Large" } ], "default": "medium" } ] }]6. Color Presets
Section titled “6. Color Presets”Provide predefined color combinations for quick theming:
"presets": [ { "name": "Default", "settings": {} }, { "name": "Ocean", "settings": { "primary": "#0ea5e9", "secondary": "#06b6d4", "accent": "#14b8a6" } }, { "name": "Forest", "settings": { "primary": "#22c55e", "secondary": "#16a34a", "accent": "#84cc16" } }]Creating Section Templates
Section titled “Creating Section Templates”Section templates define the HTML structure for page building blocks. Each template has:
- A Jinja2 template file (
.jinja) - A settings schema
- Default settings values
Section Template Definition (templates/section_templates.yaml)
Section titled “Section Template Definition (templates/section_templates.yaml)”[ { "data_type": "WebsiteSectionTemplate", "identifier": "hero_centered", "name": "Hero Centered", "category": "Hero", "icon": "IconLayoutRows", "sequence": 1, "template": "hero_centered.jinja", "schema": [ { "type": "text", "id": "heading", "label": "Heading" }, { "type": "textarea", "id": "subheading", "label": "Subheading" }, { "type": "image", "id": "background_image", "label": "Background Image" }, { "type": "checkbox", "id": "show_buttons", "label": "Show Buttons", "default": true }, { "type": "text", "id": "primary_button_text", "label": "Primary Button Text" }, { "type": "url", "id": "primary_button_link", "label": "Primary Button Link" } ], "default_settings": { "heading": "Build Something Amazing", "subheading": "The modern platform for growing businesses.", "background_image": "", "show_buttons": true, "primary_button_text": "Get Started", "primary_button_link": "/contact" } }]Section Template Fields
Section titled “Section Template Fields”| Field | Description |
|---|---|
identifier | Unique identifier (same as base = override) |
name | Display name in section picker |
category | Category: Hero, Features, Content, CTA, Testimonials, Pricing, FAQ, Team, Contact, Footer |
icon | Tabler icon name |
sequence | Display order in section picker |
template | Jinja2 template filename (in same folder) |
schema | Settings schema for section editor |
default_settings | Default values for settings |
Setting Types
Section titled “Setting Types”| Type | Description | Properties |
|---|---|---|
text | Single line text | id, label, default, placeholder |
textarea | Multi-line text | id, label, default, placeholder |
color | Color picker | id, label, default |
image | Image picker | id, label, default |
url | URL input | id, label, default, placeholder |
email | Email input | id, label, default |
checkbox | Boolean toggle | id, label, default |
number | Number input | id, label, default, min, max, step |
select | Dropdown | id, label, default, options |
header | Section header (display only) | content |
Select Options Format
Section titled “Select Options Format”{ "type": "select", "id": "image_position", "label": "Image Position", "options": [ { "value": "left", "label": "Left" }, { "value": "right", "label": "Right" } ], "default": "right"}Jinja2 Template Example
Section titled “Jinja2 Template Example”Templates have access to:
settings- Merged section settings (defaults + user overrides)theme_variables- Theme color/font settingsnow()- Current datetime function
{#- Hero Centered Section -#}{%- set tv = theme_variables or {} -%}{%- set primary = tv.primary or '#4f46e5' -%}{%- set secondary = tv.secondary or '#7c3aed' -%}
<section class="section-hero hero-centered py-5"> <div class="container"> <div class="row justify-content-center"> <div class="col-lg-8 text-center"> {% if settings.heading %} <h1 class="display-4 fw-bold mb-4">{{ settings.heading }}</h1> {% endif %}
{% if settings.subheading %} <p class="lead text-muted mb-4">{{ settings.subheading }}</p> {% endif %}
{% if settings.show_buttons %} <div class="d-flex gap-3 justify-content-center"> {% if settings.primary_button_text %} <a href="{{ settings.primary_button_link or '#' }}" class="btn btn-primary btn-lg"> {{ settings.primary_button_text }} <i class="bi bi-arrow-right ms-2"></i> </a> {% endif %} </div> {% endif %} </div> </div> </div></section>Complex Settings (Arrays)
Section titled “Complex Settings (Arrays)”For repeatable content like features or testimonials, define arrays in default_settings:
"default_settings": { "heading": "Our Features", "features": [ { "title": "Easy to Use", "description": "Simple interface", "icon": "hand-thumbs-up" }, { "title": "Fast", "description": "Lightning performance", "icon": "lightning" }, { "title": "Secure", "description": "Enterprise security", "icon": "shield-check" } ]}Iterate in template:
{% for feature in settings.features or [] %}<div class="col-md-4"> <div class="feature-icon" style="background: linear-gradient(135deg, {{ primary }} 0%, {{ secondary }} 100%);"> <i class="bi bi-{{ feature.icon }}"></i> </div> <h5>{{ feature.title }}</h5> <p>{{ feature.description }}</p></div>{% endfor %}Theme JavaScript
Section titled “Theme JavaScript”Theme JavaScript should use the Preact-based fullfinity framework for consistency and proper lifecycle management.
Basic Component Pattern
Section titled “Basic Component Pattern”(function() { 'use strict';
function initTheme() { if (!window.fullfinity) { setTimeout(initTheme, 50); return; }
const { registry, useEffect, useRef, useState, html } = window.fullfinity;
// Navbar scroll effect registry.component('MyThemeNavbarEffect', function MyThemeNavbarEffect() { useEffect(() => { const navbar = document.querySelector('.navbar'); if (!navbar) return;
const onScroll = () => { if (window.scrollY > 50) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } };
window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); // Initial check
// Cleanup on unmount return () => window.removeEventListener('scroll', onScroll); }, []);
return null; // Non-visual component });
// Scroll animations registry.component('MyThemeScrollAnimation', function MyThemeScrollAnimation() { useEffect(() => { if (!('IntersectionObserver' in window)) { document.querySelectorAll('[data-animate]').forEach(el => { el.classList.add('animated'); }); return; }
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animated'); observer.unobserve(entry.target); } }); }, { threshold: 0.1 });
document.querySelectorAll('[data-animate]').forEach(el => observer.observe(el));
return () => observer.disconnect(); }, []);
return null; });
// Auto-mount components const components = ['MyThemeNavbarEffect', 'MyThemeScrollAnimation']; components.forEach(name => { if (!document.querySelector(`[data-component="${name}"]`)) { const el = document.createElement('div'); el.setAttribute('data-component', name); el.style.display = 'none'; document.body.appendChild(el); } });
console.log('[MyTheme] Initialized'); }
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTheme); } else { initTheme(); }})();Available Framework Features
Section titled “Available Framework Features”| Feature | Usage |
|---|---|
registry.component(name, fn) | Register a component |
useState, useEffect, useRef | Preact hooks |
html\…“ | Tagged template for JSX-like syntax |
signal(), computed(), effect() | Preact signals |
CSS Best Practices
Section titled “CSS Best Practices”1. Use CSS Variables
Section titled “1. Use CSS Variables”:root { --theme-primary: #4f46e5; --theme-secondary: #7c3aed; --theme-radius: 8px;}
.btn-primary { background: var(--theme-primary); border-radius: var(--theme-radius);}2. Scope Section Styles
Section titled “2. Scope Section Styles”Prefix styles with section class names to avoid conflicts:
.section-hero .hero-title { font-size: 3.5rem; letter-spacing: -0.03em;}
.section-features .feature-card { border: 1px solid var(--theme-border); border-radius: var(--theme-radius);}3. Responsive Design
Section titled “3. Responsive Design”Use Bootstrap breakpoints and mobile-first approach:
.section-hero .hero-title { font-size: 2rem;}
@media (min-width: 768px) { .section-hero .hero-title { font-size: 2.5rem; }}
@media (min-width: 992px) { .section-hero .hero-title { font-size: 3.5rem; }}4. Animations
Section titled “4. Animations”Define reusable animations:
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); }}
[data-animate] { opacity: 0;}
[data-animate].animated { animation: fadeInUp 0.6s ease forwards;}Using Icons
Section titled “Using Icons”Themes use Bootstrap Icons. The icon CSS is included automatically.
In templates, use the full icon class:
<i class="bi bi-arrow-right"></i>For dynamic icons from settings, store just the icon name:
{ "icon": "rocket-takeoff" }Then in template:
<i class="bi bi-{{ feature.icon }}"></i>Optional: Post-Install Hook
Section titled “Optional: Post-Install Hook”Create __init__.py to run code when the theme is installed:
from fullfinity.engine.context import env_ctx
async def post_install(): """Set this theme as default for websites without a theme.""" env = env_ctx.get() Website = env("Website") WebsiteTheme = env("WebsiteTheme")
theme = await WebsiteTheme.filter(identifier="website_theme_mytheme").first() if not theme: return
# Set as default for websites without a theme websites = await Website.filter(theme__isnull=True).all() for website in websites: website.theme = theme.id await website.save()- Test with different content lengths - Ensure sections handle long/short text gracefully
- Mobile-first - Use Bootstrap’s responsive utilities and test on mobile
- Accessibility - Use semantic HTML, proper heading hierarchy, and sufficient color contrast
- Performance - Minimize custom CSS/JS, use efficient selectors
- Fallbacks - Always provide default values in templates:
{{ settings.heading or 'Default Heading' }}
- Theme variables - Access theme colors in templates via
theme_variables:{%- set tv = theme_variables or {} -%}{%- set primary = tv.primary or '#4f46e5' -%}