This guide covers the technical implementation of CheckTick's theming system for developers. For user-facing theme configuration, see Branding and Theme Settings. For platform deployment configuration, see Self-Hosting: Platform Theme Configuration.
Note: Platform-level branding configuration (logos, themes, fonts) is available to:
- Enterprise tier users on hosted CheckTick (via /branding/ web UI)
- Superusers on self-hosted deployments (via web UI or manage.py configure_branding CLI)
Technology Stack
- Tailwind CSS v4.1.17 - Utility-first CSS framework with CSS-based configuration
- daisyUI v5.4.7 - Component library with 32 built-in theme presets
- @tailwindcss/typography - Rich text styling for prose content
- @tailwindcss/cli v4.1.17 - Separate build tool package for Tailwind v4
Tailwind CSS v4 Configuration
Tailwind CSS v4 uses CSS-based configuration instead of JavaScript config files:
- Entry point:
checktick_app/static/css/daisyui_themes.css - Configuration: Uses
@import,@plugin, and@themedirectives in CSS - Build tool:
@tailwindcss/cliv4.1.17 (separate package from Tailwind) - No
tailwind.config.js: All configuration is in CSS files
Example configuration structure:
@import "tailwindcss";
@plugin "daisyui" { themes: all; }
@plugin "@tailwindcss/typography";
Key Differences from Tailwind v3
- CSS-first configuration - No JavaScript config file required
- Separate CLI package -
@tailwindcss/cliinstead oftailwindcssCLI - New
@directives -@import,@plugin,@theme,@source - Native CSS features - Better integration with modern CSS
daisyUI v5 Themes
All 32 daisyUI themes are loaded in CheckTick:
20 Light Themes:
- light, cupcake, bumblebee, emerald, corporate, retro, cyberpunk, valentine, garden, lofi, pastel, fantasy, nord, cmyk, autumn, acid, lemonade, winter, nord, sunset
12 Dark Themes:
- dark, synthwave, halloween, forest, aqua, black, luxury, dracula, business, night, coffee, dim
Themes are applied via the data-theme attribute on <html> or <body>:
<html data-theme="nord">
Logical Naming System
CheckTick uses a logical naming system to separate user preferences from actual daisyUI presets:
- checktick-light (logical name) → maps to selected light preset (default: "nord")
- checktick-dark (logical name) → maps to selected dark preset (default: "business")
- JavaScript automatically applies the correct daisyUI preset based on configuration
Why? This allows changing platform default themes without breaking user preferences. A user who selected "light mode" will automatically get the new light preset if the platform admin changes it.
Theme Architecture
File Structure
checktick_app/
├── static/
│ ├── css/
│ │ └── daisyui_themes.css # Tailwind v4 entry point
│ ├── build/
│ │ └── styles.css # Built output (minified)
│ └── js/
│ ├── theme-toggle.js # User theme switcher
│ └── admin-theme.js # Admin theme switcher
├── core/
│ ├── themes.py # Theme utilities (presets, parsing)
│ ├── models.py # SiteBranding model
│ └── views.py # Theme update handlers
├── surveys/
│ └── models.py # Organization, Survey models
├── context_processors.py # Theme cascade logic
└── templates/
├── base.html # Main template with theme
├── base_minimal.html # Minimal template
└── admin/
└── base_site.html # Admin template override
Theme Cascade Logic
The context processor (checktick_app/context_processors.py) implements the 3-tier hierarchy:
# 1. Start with platform defaults (environment vars + SiteBranding)
preset_light = settings.BRAND_THEME_PRESET_LIGHT or "nord"
preset_dark = settings.BRAND_THEME_PRESET_DARK or "business"
if SiteBranding:
sb = SiteBranding.objects.first()
if sb:
preset_light = sb.theme_preset_light or preset_light
preset_dark = sb.theme_preset_dark or preset_dark
# 2. Check for organization-level override
user_org = None
if user and user.is_authenticated:
# Get user's primary organization
user_org = Organization.objects.filter(owner=user).first()
if not user_org:
membership = OrganizationMembership.objects.filter(user=user).first()
if membership:
user_org = membership.organization
# 3. Apply organization theme if set
if user_org and (user_org.theme_preset_light or user_org.theme_preset_dark):
preset_light = user_org.theme_preset_light or preset_light
preset_dark = user_org.theme_preset_dark or preset_dark
# Also apply custom CSS if provided
if user_org.theme_light_css:
theme_css_light = user_org.theme_light_css
if user_org.theme_dark_css:
theme_css_dark = user_org.theme_dark_css
# 4. Survey-level overrides handled per-template (not in context processor)
Database Models
SiteBranding (platform-level):
class SiteBranding(models.Model):
default_theme = models.CharField(max_length=64) # checktick-light/dark
theme_preset_light = models.CharField(max_length=64) # nord, etc.
theme_preset_dark = models.CharField(max_length=64) # business, etc.
theme_light_css = models.TextField() # Custom CSS variables
theme_dark_css = models.TextField() # Custom CSS variables
icon_file = models.FileField() # Uploaded icon
icon_url = models.URLField() # Or icon URL
# ... font fields, dark mode icon fields
Organization (organization-level):
class Organization(models.Model):
name = models.CharField(max_length=255)
owner = models.ForeignKey(User)
default_theme = models.CharField(max_length=64, blank=True)
theme_preset_light = models.CharField(max_length=64, blank=True)
theme_preset_dark = models.CharField(max_length=64, blank=True)
theme_light_css = models.TextField(blank=True)
theme_dark_css = models.TextField(blank=True)
Survey (survey-level):
class Survey(models.Model):
owner = models.ForeignKey(User)
organization = models.ForeignKey(Organization, null=True, blank=True)
style = models.JSONField(default=dict) # Flexible styling
# style structure:
# {
# "custom_css": "...",
# "theme_light": "cupcake",
# "theme_dark": "forest",
# "fonts": {...}
# }
CSS Build Process
Build Configuration
package.json script:
{
"scripts": {
"build:css": "tailwindcss --input checktick_app/static/css/daisyui_themes.css --output checktick_app/static/build/styles.css --minify"
}
}
Build Details
- Input:
checktick_app/static/css/daisyui_themes.css - Output:
checktick_app/static/build/styles.css(minified) - Build time: ~250ms
- Output size: ~192KB (includes all 39 daisyUI themes)
- Watch mode:
npm run build:css -- --watch
When to Rebuild
Rebuild CSS when:
- Changing Tailwind/daisyUI configuration in CSS files
- Adding new templates with utility classes
- Modifying component styles in CSS
- Updating
@pluginor@themedirectives
Development (Docker):
docker compose exec web npm run build:css
Production:
npm run build:css
python manage.py collectstatic --noinput
Theme Utilities
themes.py Module
Location: checktick_app/core/themes.py
Constants:
LIGHT_THEMES = [
"light", "cupcake", "bumblebee", "emerald", "corporate", "retro",
"cyberpunk", "valentine", "garden", "lofi", "pastel", "fantasy",
"nord", "cmyk", "autumn", "acid", "lemonade", "winter", "nord", "sunset"
]
DARK_THEMES = [
"dark", "synthwave", "halloween", "forest", "aqua", "black",
"luxury", "dracula", "business", "night", "coffee", "dim"
]
Functions:
get_theme_color_scheme(theme_name: str) -> str
- Returns "light" or "dark" for a given theme preset name
- Used to determine appropriate meta tags and prefers-color-scheme
normalize_daisyui_builder_css(raw_css: str) -> str
- Normalizes CSS from daisyUI Theme Generator
- Strips selectors, extracts variables only
- Maps builder variables to daisyUI runtime variables
generate_theme_css_for_brand(preset_light, preset_dark, custom_light_css, custom_dark_css) -> tuple
- Generates complete theme CSS for both modes
- Merges preset with custom CSS overrides
- Returns
(light_css, dark_css)tuple - Used by SiteBranding and Organization models
parse_custom_theme_config(config: dict) -> dict
- Parses survey
styleJSONField - Extracts theme presets, custom CSS, fonts
- Returns normalized configuration dict
Example Usage
from checktick_app.core.themes import (
LIGHT_THEMES,
get_theme_color_scheme,
generate_theme_css_for_brand
)
# Check theme color scheme
scheme = get_theme_color_scheme("nord") # "light"
# Generate theme CSS
light_css, dark_css = generate_theme_css_for_brand(
preset_light="nord",
preset_dark="business",
custom_light_css="--color-primary: oklch(65% 0.21 25);",
custom_dark_css="--color-primary: oklch(45% 0.18 25);"
)
Project-Level vs Organization-Level vs Survey-Level Theming
1. Platform-Level (Global)
- Who: Organization admin (superuser) in the Profile page
- Applies to: Entire site by default
- What you can configure:
- Theme presets: Choose from 20 light themes and 12 dark themes (daisyUI v5.4.7 presets)
- Default light:
nord(clean, minimal design) - Default dark:
business(professional dark theme) - Can be changed via dropdown selectors in Profile page
- Default light:
- Advanced custom CSS: Optional custom theme CSS from the daisyUI Theme Generator that overrides the selected presets
- Site icon (favicon): upload SVG/PNG or provide a URL
- Dark-mode icon: upload a separate SVG/PNG or provide a URL (used when the dark theme is active)
- Fonts: heading/body stacks and an optional external Font CSS URL (e.g. Google Fonts)
- Where it's stored:
SiteBrandingmodel in the database (theme_preset_light,theme_preset_dark,theme_css_light,theme_css_dark) - How it's applied:
- The base template (
base.html) uses the configured presets in thedata-themeattribute - The logical theme names
checktick-lightandchecktick-darkare mapped to the actual preset names (e.g.,nordorbusiness) - Custom CSS from the theme generator is injected as CSS variables under the
checktick-lightandchecktick-darktheme selectors - Theme switching happens via JavaScript that maps the logical names to the actual presets
-
Environment variables
BRAND_THEME_PRESET_LIGHTandBRAND_THEME_PRESET_DARKprovide deployment-level defaults -
Survey-level theming — per-survey customization
-
Who: Survey owners/managers
- Applies to: Specific survey views (dashboard, detail, groups, and builder)
- What you can configure:
- Optional title/icon override
- Fonts (heading/body stacks and font CSS URL)
- Theme CSS overrides for light/dark from the daisyUI builder (variables only)
- How it’s applied:
- Survey templates include a
head_theme_overridesblock to inject per-survey font CSS and daisyUI variable overrides, and anicon_linkblock to set a per-survey favicon. - Per-survey overrides take precedence on those pages because they’re injected in-page.
Precedence and merge behavior
- Base daisyUI presets (e.g.,
nord,business) provide the foundation - Project-level custom CSS from Theme Generator refines the preset across the entire site
- Survey-level overrides win on survey pages where they're included
- Avoid mixing heavy global CSS with inline colors; prefer daisyUI variables so all layers compose cleanly
How to configure project-level theming
-
Go to Profile → Project theme and brand (admin-only)
-
Choose theme presets:
-
Light theme preset: Select from 20 options (default:
nord) - Dark theme preset: Select from 12 options (default:
business) -
The logical names
checktick-lightandchecktick-darkare preserved for compatibility -
Set branding:
-
Icon: either upload an SVG/PNG or paste an absolute URL
- Dark mode icon: optional separate icon for dark theme
-
Fonts: set heading/body stacks; optionally paste a Font CSS URL (e.g. Google Fonts)
-
Advanced: Custom Theme CSS (optional):
-
For power users: paste CSS variables from the daisyUI Theme Generator
- Copy variables for both light and dark themes if you need precise control
- Paste into "Light theme CSS" and "Dark theme CSS" fields
- Custom CSS overrides the selected preset
-
We normalize builder variables into daisyUI runtime variables and inject them under the
checktick-lightandchecktick-darktheme selectors -
Save — the base template will now serve your icon, fonts, and theme colors sitewide
Tip: Most users should just select a preset. Custom CSS is only needed for unique branding requirements.
How to configure survey-level theming
-
Open a survey → Dashboard → "Survey style"
-
Optional: set a Title override and Icon URL
-
Fonts: set heading/body stacks and optionally a Font CSS URL
-
Theme name: normally leave as-is unless you're switching between daisyUI themes
-
Primary color: provide a hex like
#ff3366; the server will convert it to the appropriate color space / variables -
If you have daisyUI builder variables for this survey's unique palette:
-
Paste the light/dark sets in their respective fields (where available)
- The page will inject them under
[data-theme="checktick-light"]and[data-theme="checktick-dark"]
These overrides only apply on survey pages and do not affect the rest of the site.
Acceptable daisyUI builder CSS
Note: Most users should just select a theme preset in Profile settings. This section is for advanced customization only.
Paste only variable assignments from the daisyUI Theme Generator, for example:
--color-primary: oklch(65% 0.21 25);
--radius-selector: 1rem;
--depth: 0;
We map these to daisyUI runtime variables (e.g., --p, --b1, etc.) and inject them under the checktick-light and checktick-dark theme selectors. Avoid pasting arbitrary CSS rules; stick to variables for predictable results.
Troubleshooting
- Colors don't apply: Check for any hardcoded inline CSS overriding CSS variables. Prefer variables and themes.
- Wrong theme shown after changes: Your theme selection is cached in browser localStorage. Use the light/dark toggle in your profile or clear localStorage to reset.
- Preset not applying: Make sure you saved the profile settings and refreshed the browser. Check browser DevTools to see the
data-themeattribute on<html>. - Icon not showing: If you uploaded an icon, make sure media is configured. If using a URL, verify it's reachable. The app falls back to a default SVG if none is set.
Typography and button links
@tailwindcss/typography styles content in .prose, including links. To avoid underlines and color overrides on daisyUI button anchors, the build loads Typography first and daisyUI second, with a small Typography override to skip underlines on a.btn.
- To opt a specific element out of Typography effects, add
not-prose.
Rendering forms with daisyUI
We ship a filter and partials to standardize Django form rendering.
Template filter: add_classes
File: checktick_app/surveys/templatetags/form_extras.py
Usage:
{% load form_extras %}
{{ form.field|add_classes:"input input-bordered w-full" }}
Partial: components/form_field.html
File: checktick_app/templates/components/form_field.html
Context:
field(required): bound Django form fieldlabel(optional)help(optional)classes(optional): override default classes
Defaults when classes isn’t provided:
- Text-like inputs:
input input-bordered w-full - Textarea:
textarea textarea-bordered w-full - Select:
select select-bordered w-full
Example:
{% include "components/form_field.html" with field=form.name label="Name" %}
{% include "components/form_field.html" with field=form.slug label="URL Name or 'Slug' (optional)" help="If left blank, a slug will be generated from the name." %}
{% include "components/form_field.html" with field=form.description label="Description" %}
Render an entire form
Helper that iterates over visible fields:
{% include "components/render_form_fields.html" with form=form %}
This uses form_field for each field. For radio/checkbox groups needing custom layout, render bespoke markup with daisyUI components or pass classes explicitly.
Choice components
For grouped choices, use the specialized components:
components/radio_group.html— radios with daisyUIcomponents/checkbox_group.html— checkboxes with daisyUI
Examples:
{% include "components/radio_group.html" with name="account_type" label="Account type" choices=(("simple","Simple user"),("org","Organisation")) selected='simple' inline=True %}
{% include "components/checkbox_group.html" with name="interests" label="Interests" choices=(("a","A"),("b","B")) selected=("a",) %}
Rebuild CSS
Tailwind CSS v4 uses CSS-based configuration instead of tailwind.config.js. Configuration is done via @import and @plugin directives in CSS files.
Whenever you change CSS files or add new templates:
npm run build:css
If running under Docker, rebuild the image or ensure your build step runs inside the container.
Tailwind v4 architecture
- CLI: Uses
@tailwindcss/clipackage (separate from maintailwindcsspackage) - No config file: Configuration moved to CSS using
@import,@plugin, and@themedirectives - daisyUI v5: All 39 themes loaded via
@plugin "daisyui" { themes: all; }indaisyui_themes.css
Single stylesheet entry
- Unified Tailwind/daisyUI input:
checktick_app/static/css/daisyui_themes.css - Built output:
checktick_app/static/build/styles.css - Loaded globally in
checktick_app/templates/base.htmlvia{% static 'build/styles.css' %}
Do not add other <link rel="stylesheet"> tags or separate CSS files; extend styling through Tailwind utilities, daisyUI components, or minimal additions inside the unified entry file.
Breadcrumbs component
We ship a reusable DaisyUI-style breadcrumbs component with icons.
- File:
checktick_app/templates/components/breadcrumbs.html - Purpose: Provide consistent navigation crumbs across survey pages
- Icons:
- Survey: clipboard icon
- Question group: multiple documents icon
- Question (current): single document icon
How to use
There are two ways to render breadcrumbs, depending on what’s most convenient in your template.
- Numbered crumb parameters (template-friendly)
Pass labeled crumbs in order. For any crumb you pass, you can optionally include an *_href to make it a link. The last crumb usually omits *_href to indicate the current page.
{% include 'components/breadcrumbs.html' with
crumb1_label="Survey Dashboard"
crumb1_href="/surveys/"|add:survey.slug|add:"/dashboard/"
crumb2_label="Question Group Builder"
crumb2_href="/surveys/"|add:survey.slug|add:"/builder/"
crumb3_label="Question Builder"
%}
- Items iterable (tuple list)
If you already have a list, pass items as an iterable of (label, href) tuples. Use None for href on the current page.
{% include 'components/breadcrumbs.html' with
items=(("Survey Dashboard", "/surveys/"|add:survey.slug|add:"/dashboard/"),
("Question Group Builder", "/surveys/"|add:survey.slug|add:"/builder/"),
("Question Builder", None))
%}
Styling
Breadcrumbs inherit DaisyUI theme colors and are further tuned globally so that:
- Links are lighter by default and only underline on hover
- The current (non-link) crumb is slightly lighter to indicate context
These tweaks live in the single CSS entry at checktick_app/static/src/tailwind.css in a small component layer block:
@layer components {
.breadcrumbs a {
@apply no-underline text-base-content/70 hover:underline hover:text-base-content/90;
}
.breadcrumbs li > span {
@apply text-base-content/60;
}
/* Ensure Typography (.prose) doesn’t re-add underlines */
.prose :where(.breadcrumbs a):not(:where([class~="not-prose"])) {
@apply no-underline text-base-content/70 hover:underline hover:text-base-content/90;
}
}
Any updates here require a CSS rebuild.
Page conventions
- Survey dashboard pages begin with a clipboard icon crumb (Survey)
- Survey-level builder links (groups) show multiple documents
- Group-level question builder shows a single document for the active page
Keep breadcrumb labels terse and consistent (e.g., “Survey Dashboard”, “Question Group Builder”, “Question Builder”).
Internationalization (i18n)
This project ships with Django i18n enabled. Themes and UI copy should use Django’s translation tags and helpers so labels, buttons, and help text can be translated cleanly without forking templates.
Template basics
- Load i18n in templates that have translatable text:
{% load i18n %}
- Translate short strings:
{% trans "Manage groups" %}
- Translate sentences with variables using blocktrans:
{% blocktrans %}Groups for {{ survey.name }}{% endblocktrans %}
- Prefer assigning translated values to variables when you need them inside attributes (e.g., placeholders) or component includes (breadcrumbs):
{% trans "Surveys" as bc_surveys %}
{% include 'components/breadcrumbs.html' with crumb1_label=bc_surveys crumb1_href="/surveys/" %}
{% trans "Defaults to platform title" as ph_title %}
<input placeholder="{{ ph_title }}" />
- Plurals with blocktrans:
{% blocktrans count q=group.q_count %}
{{ q }} question
{% plural %}
{{ q }} questions
{% endblocktrans %}
Notes
- Don’t wrap dynamic values (like
survey.name) in{% trans %}; translate only the surrounding text. - Keep punctuation and capitalization stable to help translators.
- For long help text, use
{% blocktrans %}to keep the string intact for translators.
Python code
Use Django’s translation utilities in Python code:
from django.utils.translation import gettext as _
from django.utils.translation import ngettext
msg = _("You don’t have permission to edit this survey.")
label = ngettext(
"{count} response", # singular
"{count} responses", # plural
total,
).format(count=total)
For lazily-evaluated strings in model fields or settings, prefer gettext_lazy:
from django.utils.translation import gettext_lazy as _
class MyForm(forms.Form):
name = forms.CharField(label=_("Name"))
Message extraction and compilation (Docker + Poetry)
Run these inside the web container so they use the project environment. Replace fr with your target language code (ISO 639-1).
# Create/update .po files for templates and Python strings
docker compose exec web poetry run python manage.py makemessages -l fr
# Optionally, extract JavaScript strings (if using Django’s JS i18n)
docker compose exec web poetry run python manage.py makemessages -d djangojs -l fr
# After translating the .po files, compile to .mo
docker compose exec web poetry run python manage.py compilemessages
Defaults and structure
- Locale files can live per app (e.g.,
checktick_app/surveys/locale/) or at the project rootlocale/. Django will discover both. - Ensure
USE_I18N = Truein settings (it is by default in this project). - Ignore build and vendor dirs during extraction to avoid noise (Django’s
makemessagesrespects.gitignoreand you can add-ipatterns if needed).
Theming + i18n tips
- Translate UI labels, headings, and helper text; leave CSS variables and class names untouched.
- When using DaisyUI/Tailwind inside
.prose, prefer wrapping button links with.btnso Typography doesn’t restyle them. Translations won’t affect classes. - Breadcrumbs: translate labels via
{% trans %} … as var %}before passing to the component. - For placeholders/tooltips, assign translated strings to variables and reference them in attributes.
Theme selection (System/Light/Dark)
End users can choose how the UI looks on the Profile page. The selector supports:
- System — follow the operating system’s preference (auto-switches if the OS changes)
- Light — force the custom light theme (
checktick-light) - Dark — force the custom dark theme (
checktick-dark)
How it works:
- The active preference is saved to
localStorageunder the keychecktick-theme. - Accepted values:
system,checktick-light,checktick-dark. - On first visit, the server’s default (
data-themeon<html>) is used; if it matches the system preference, the selector showsSystem. - Changing the selector immediately updates
html[data-theme]and persists the choice. - When
Systemis selected, the UI updates automatically on OS theme changes viaprefers-color-scheme.
Relevant files:
- Profile UI:
checktick_app/core/templates/core/profile.html - Runtime logic:
checktick_app/static/js/theme-toggle.js - Script is loaded in:
checktick_app/templates/base.html
Branding and customization
This project supports organization branding at the platform level with sensible accessibility defaults and light/dark variants.
Navbar brand icon
- The navbar shows the brand icon next to the site title.
- Source priority (first available wins):
1) Uploaded file on the Profile page (light:
icon_file, dark:icon_file_dark) 2) URL saved on the Profile page (light:icon_url, dark:icon_url_dark) 3) Django settings (BRAND_ICON_URL,BRAND_ICON_URL_DARK) 4) Inline SVG fallback (a neutral stroke-based mark) - Dark mode: if a dark icon is set (uploaded or URL), it is shown automatically when the active theme contains
checktick-dark. - The icon includes
altandtitleattributes derived fromBRAND_ICON_ALTandBRAND_ICON_TITLE(defaulting to the site title). - Size can be customized with
BRAND_ICON_SIZE_CLASS(Tailwind classes likew-8 h-8) orBRAND_ICON_SIZE(number ->w-{n} h-{n}). Defaults tow-6 h-6.
Accessibility:
- Icons are rendered with
alt/titleand, for inline SVG,role="img"andaria-labelto ensure assistive technology support. - Prefer high-contrast icons. If providing separate light/dark assets, test both on your themes.
Fonts and CSS variables
- Heading/body font stacks are applied via CSS variables (
--font-headingand--font-body). - Optional
font_css_urlallows fast integration with Google Fonts or similar. Ensure stacks match the families you load.
Survey-specific overrides
- Surveys can override icon, fonts, and DaisyUI variables on their pages. See “How to configure survey-level theming” above.
Rebuild reminder
- Changes to Tailwind/DaisyUI configs or new templates require rebuilding the CSS bundle.