Practical guide to building professional MintyFlask themes - patterns, customisation, and best practices

Theme Creation User Guide

Overview

This guide covers practical theme development in MintyFlask. It assumes you've read the Quick Start and have created a basic theme.

What you'll learn:

  • Advanced template customisation patterns
  • Design token strategy and management
  • Multi-mixin integration
  • Theme variants and inheritance
  • Performance optimisation
  • Production deployment

Theme Architecture

A MintyFlask theme consists of three layers working together:

Layer 1: Foundation (From _core)

  • Base templates (base.html, single.html, list.html)
  • Foundation CSS (typography, tokens, core components)
  • Core JavaScript (theme switcher, utilities)

Layer 2: Mixins (From mixins/*)

  • Feature layouts (post-list.html, blog-home.html)
  • Component CSS (.blog-card, .portfolio-grid)
  • Feature macros and partials

Layer 3: Theme (Your customisation)

  • Page templates (using mixin layouts)
  • Theme CSS (token overrides, component customisation)
  • Theme-specific components and layouts

Design Token Strategy

Token Categories

Organise your tokens into logical categories:

/* themes/my-blog/static/css/foundations/tokens.css */

@layer theme {
  :root {
    /* === Colour Palette === */

    /* Brand colours */
    --color-brand-primary: oklch(60% 0.15 250);
    --color-brand-secondary: oklch(65% 0.12 180);
    --color-brand-accent: oklch(70% 0.18 45);

    /* Semantic colours */
    --color-success: oklch(65% 0.15 145);
    --color-warning: oklch(70% 0.15 85);
    --color-error: oklch(60% 0.15 25);
    --color-info: oklch(65% 0.12 240);

    /* Surface colours */
    --color-surface: oklch(98% 0 0);
    --color-surface-elevated: oklch(99% 0 0);
    --color-surface-sunken: oklch(96% 0 0);

    /* Text colours */
    --color-text-primary: oklch(20% 0 0);
    --color-text-secondary: oklch(40% 0 0);
    --color-text-tertiary: oklch(60% 0 0);
    --color-text-inverted: oklch(98% 0 0);

    /* Border colours */
    --color-border: oklch(85% 0 0);
    --color-border-strong: oklch(70% 0 0);

    /* === Spacing Scale === */

    --spacing-xs: 0.25rem; /* 4px */
    --spacing-sm: 0.5rem; /* 8px */
    --spacing-md: 1rem; /* 16px */
    --spacing-lg: 1.5rem; /* 24px */
    --spacing-xl: 2rem; /* 32px */
    --spacing-2xl: 3rem; /* 48px */
    --spacing-3xl: 4rem; /* 64px */

    /* === Typography Scale === */

    --text-xs: 0.75rem; /* 12px */
    --text-sm: 0.875rem; /* 14px */
    --text-base: 1rem; /* 16px */
    --text-lg: 1.125rem; /* 18px */
    --text-xl: 1.25rem; /* 20px */
    --text-2xl: 1.5rem; /* 24px */
    --text-3xl: 1.875rem; /* 30px */
    --text-4xl: 2.25rem; /* 36px */

    /* Font weights */
    --font-weight-normal: 400;
    --font-weight-medium: 500;
    --font-weight-semibold: 600;
    --font-weight-bold: 700;

    /* Line heights */
    --line-height-tight: 1.2;
    --line-height-normal: 1.5;
    --line-height-relaxed: 1.75;

    /* === Border Radius === */

    --radius-xs: 0.125rem; /* 2px */
    --radius-sm: 0.25rem; /* 4px */
    --radius-md: 0.375rem; /* 6px */
    --radius-lg: 0.5rem; /* 8px */
    --radius-xl: 0.75rem; /* 12px */
    --radius-2xl: 1rem; /* 16px */
    --radius-full: 9999px; /* Pill shape */

    /* === Shadows === */

    --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
    --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
    --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);

    /* === Transitions === */

    --transition-fast: 150ms ease;
    --transition-base: 200ms ease;
    --transition-slow: 300ms ease;
  }
}

Dark Mode Tokens

@layer theme {
  .dark {
    /* Brand colours (slightly adjusted for dark mode) */
    --color-brand-primary: oklch(65% 0.15 250);
    --color-brand-secondary: oklch(70% 0.12 180);

    /* Surface colours */
    --color-surface: oklch(20% 0 0);
    --color-surface-elevated: oklch(25% 0 0);
    --color-surface-sunken: oklch(15% 0 0);

    /* Text colours */
    --color-text-primary: oklch(95% 0 0);
    --color-text-secondary: oklch(75% 0 0);
    --color-text-tertiary: oklch(60% 0 0);

    /* Border colours */
    --color-border: oklch(30% 0 0);
    --color-border-strong: oklch(40% 0 0);

    /* Shadows (lighter for dark mode) */
    --shadow-sm: 0 1px 2px 0 rgba(255, 255, 255, 0.05);
    --shadow-md: 0 4px 6px -1px rgba(255, 255, 255, 0.1);
    --shadow-lg: 0 10px 15px -3px rgba(255, 255, 255, 0.1);
  }
}

Advanced Template Patterns

Pattern 1: Layout Extension with Configuration

{# themes/agency/templates/pages/services.html #}
{#
 # Services page using post-list layout
 # Demonstrates extensive configuration
 #}
{% extends "layouts/post-list.html" %}

{# Page configuration #}
{% set page_title = "Our Services" %}
{% set page_description = "Professional web development services" %}
{% set page_subtitle %}
  <p class="text-lg text-slate-600 dark:text-stone-400 mb-6">
    We offer comprehensive solutions for modern web applications
  </p>
{% endset %}

{# Empty state configuration #}
{% set empty_state_title = "Services Coming Soon" %}
{% set empty_state_message = "We're preparing our service offerings." %}
{% set empty_state_icon = "M21 13.255A23.931..." %}

{# Hide standard features we don't need #}
{% set show_related_topics = false %}
{% set show_post_statistics = false %}

{# Custom empty state actions #}
{% block empty_state_actions %}
<div class="card-actions card-actions-centre">
  <a href="/contact" class="btn-base btn-intent-primary btn-size-lg">
    Contact Us
  </a>
  <a href="/about" class="btn-base btn-intent-secondary btn-size-lg">
    Learn More
  </a>
</div>
{% endblock %}

{# Additional content after main list #}
{% block additional_content %}
<div class="card-base card-intent-feature card-size-lg mt-12">
  <div class="card-body text-centre">
    <h3 class="card-title text-2xl mb-4">Custom Solutions</h3>
    <p class="card-description mb-6">
      Don't see what you need? We create custom solutions tailored to your requirements.
    </p>
    <a href="/contact" class="btn-base btn-intent-primary btn-size-lg">
      Discuss Your Project
    </a>
  </div>
</div>
{% endblock %}

Pattern 2: Custom Layout Creation

{# themes/agency/templates/layouts/two-column.html #}
{#
 # Two-column layout with sidebar
 # Extends base and provides content/sidebar blocks
 #}
{% extends resolve_base_template('base.html') %}

{% block content %}
<div class="container mx-auto px-4 py-12">
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
    {# Main content area (2/3 width) #}
    <div class="lg:col-span-2">
      {% block main_content %}
      <p>Main content goes here</p>
      {% endblock %}
    </div>

    {# Sidebar (1/3 width) #}
    <aside class="lg:col-span-1">
      {% block sidebar %}
      <div class="card-base card-intent-info sticky top-4">
        <div class="card-header">
          <h3 class="card-title">Sidebar</h3>
        </div>
        <div class="card-body">
          <p class="card-description">Sidebar content</p>
        </div>
      </div>
      {% endblock %}
    </aside>
  </div>
</div>
{% endblock %}

Using the custom layout:

{# themes/agency/templates/pages/case-study.html #}
{% extends "layouts/two-column.html" %}

{% block main_content %}
<article class="prose prose-lg dark:prose-invert max-w-none">
  <h1>{{ case_study.title }}</h1>
  {{ case_study.content|safe }}
</article>
{% endblock %}

{% block sidebar %}
<div class="space-y-6">
  {# Project details card #}
  <div class="card-base card-intent-content">
    <div class="card-header">
      <h3 class="card-title">Project Details</h3>
    </div>
    <div class="card-body space-y-2">
      <div>
        <strong>Client:</strong> {{ case_study.client }}
      </div>
      <div>
        <strong>Industry:</strong> {{ case_study.industry }}
      </div>
      <div>
        <strong>Duration:</strong> {{ case_study.duration }}
      </div>
    </div>
  </div>

  {# Technologies used #}
  <div class="card-base card-intent-info">
    <div class="card-header">
      <h3 class="card-title">Technologies</h3>
    </div>
    <div class="card-body">
      <div class="flex flex-wrap gap-2">
        {% for tech in case_study.technologies %}
        <span class="badge-base badge-intent-info badge-size-sm">
          {{ tech }}
        </span>
        {% endfor %}
      </div>
    </div>
  </div>
</div>
{% endblock %}

Pattern 3: Macro-Based Page Builder

{# themes/agency/templates/pages/landing.html #}
{% extends resolve_base_template('base.html') %}
{% from "components/sections.html" import hero, features, cta with context %}

{% block content %}
{# Hero section #}
{{ hero(
    title="Transform Your Business",
    subtitle="Professional web development solutions",
    cta_text="Get Started",
    cta_url="/contact",
    background="gradient"
) }}

{# Features section #}
{{ features(
    title="Why Choose Us",
    features=[
        {
            'icon': '🚀',
            'title': 'Fast Delivery',
            'description': 'Quick turnaround on all projects'
        },
        {
            'icon': '💎',
            'title': 'Quality Code',
            'description': 'Clean, maintainable, tested'
        },
        {
            'icon': '🎯',
            'title': 'User Focused',
            'description': 'Designed with users in mind'
        }
    ]
) }}

{# Call to action #}
{{ cta(
    title="Ready to Get Started?",
    description="Let's build something amazing together",
    primary_button="Contact Us",
    primary_url="/contact",
    secondary_button="View Work",
    secondary_url="/portfolio"
) }}
{% endblock %}

Multi-Mixin Integration

Scenario: Agency Site with Blog and Portfolio

Goal: Create an agency theme that combines blog and portfolio mixins with custom branding.

Step 1: Configure theme dependencies

# themes/agency/config/theme.yaml

name: "Agency Theme"
version: "1.0.0"
parent: "_core"
description: "Professional agency theme with blog and portfolio"

dependencies:
  templates:
    - "_core/layouts/base.html"
    - "_core/layouts/single.html"
  mixins:
    - "blog"
    - "portfolio"

features:
  - blog-posts
  - portfolio-projects
  - team-members
  - case-studies

Step 2: Import mixin CSS

/* themes/agency/static/css/src/input.css */

@layer components {
  /* Import blog mixin */
  @import "../../../../../mixins/blog/static/css/blog.css";

  /* Import portfolio mixin */
  @import "../../../../../mixins/portfolio/static/css/portfolio.css";
}

@layer theme {
  /* Harmonise styles across mixins */
  :root {
    /* Unified colour scheme */
    --color-primary: oklch(55% 0.18 230);
    --color-secondary: oklch(60% 0.15 180);

    /* Consistent spacing */
    --spacing-section: 4rem;
  }

  /* Unified card styling */
  .blog-post-card,
  .portfolio-card-base {
    border-radius: var(--radius-xl);
    border: 2px solid var(--color-border);
    transition: all var(--transition-base);
  }

  .blog-post-card:hover,
  .portfolio-card-base:hover {
    transform: translateY(-4px);
    border-color: var(--color-primary);
    box-shadow: var(--shadow-xl);
  }

  /* Unified typography */
  .blog-post-title,
  .portfolio-card-title {
    font-size: var(--text-2xl);
    font-weight: var(--font-weight-bold);
    letter-spacing: -0.02em;
  }
}

Step 3: Create integrated navigation

{# themes/agency/templates/components/main-nav.html #}

<nav class="main-navigation">
  <a href="/" class="nav-link">Home</a>
  <a href="/blog" class="nav-link">Blog</a>
  <a href="/portfolio" class="nav-link">Portfolio</a>
  <a href="/services" class="nav-link">Services</a>
  <a href="/about" class="nav-link">About</a>
  <a href="/contact" class="nav-link nav-link--cta">Contact</a>
</nav>

Managing Style Conflicts

When using multiple mixins, ensure visual harmony:

@layer theme {
  /* Override conflicting styles */

  /* Make all cards consistent */
  .blog-post-card,
  .portfolio-card-base,
  .team-member-card {
    /* Consistent structure */
    background: var(--color-surface);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--spacing-lg);

    /* Consistent interaction */
    transition: all var(--transition-base);
    cursor: pointer;
  }

  /* Consistent hover states */
  .blog-post-card:hover,
  .portfolio-card-base:hover,
  .team-member-card:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-lg);
    border-color: var(--color-primary);
  }

  /* Consistent typography */
  .blog-post-title,
  .portfolio-card-title,
  .team-member-name {
    font-family: var(--font-heading);
    font-weight: var(--font-weight-bold);
    color: var(--color-text-primary);
  }
}

Performance Optimisation

CSS Optimisation

1. Purge unused styles:

// themes/agency/tailwind.config.js

module.exports = {
  content: [
    "./templates/**/*.html",
    "./templates/**/*.jinja",
    "../_core/templates/**/*.html",
    "../../mixins/*/templates/**/*.html",
  ],
  // ... other config
};

2. Minimise custom CSS:

/* themes/agency/static/css/src/input.css */

@layer theme {
  /* Only override what's necessary */
  :root {
    --color-primary: oklch(55% 0.18 230);
  }

  /* Don't duplicate entire components */
  .blog-post-card {
    border-radius: var(--radius-xl); /* Just the change */
  }
}

3. Build for production:

# Minified production build
npx tailwindcss -i input.css -o output.css --minify

# With PostCSS optimizations
npx postcss input.css -o output.css --env production

Template Optimisation

1. Use template caching:

# config.py
TEMPLATES_AUTO_RELOAD = False  # Production
SEND_FILE_MAX_AGE_DEFAULT = 31536000  # 1 year

2. Lazy load images:

<img
  src="project.jpg"
  alt="Project"
  loading="lazy"
  class="portfolio-card-image"
/>

3. Preload critical resources:

<!-- In base.html head -->
<link
  rel="preload"
  href="{{ url_for('static', filename='css/dist/output.css') }}"
  as="style"
/>
<link
  rel="preload"
  href="{{ url_for('static', filename='fonts/heading.woff2') }}"
  as="font"
  type="font/woff2"
  crossorigin
/>

Theme Variants

Create theme variants that share base styling:

Base theme structure:

themes/
├── agency-base/              # Base theme
│   └── config/
│       └── theme.yaml
├── agency-minimal/           # Minimal variant
│   ├── config/
│   │   └── theme.yaml
│   └── static/css/src/
│       └── input.css
└── agency-bold/              # Bold variant
    ├── config/
    │   └── theme.yaml
    └── static/css/src/
        └── input.css

Minimal variant:

# themes/agency-minimal/config/theme.yaml

name: "Agency Minimal"
version: "1.0.0"
parent: "agency-base"
description: "Minimal variant of agency theme"
/* themes/agency-minimal/static/css/src/input.css */

@import "../../../agency-base/static/css/src/input.css";

@layer theme {
  :root {
    /* Minimal colour palette */
    --color-primary: oklch(20% 0 0);
    --color-secondary: oklch(40% 0 0);

    /* Minimal spacing */
    --spacing-md: 0.75rem;
    --spacing-lg: 1.25rem;
  }

  /* Remove decorative elements */
  .blog-post-card,
  .portfolio-card-base {
    border: none;
    border-bottom: 1px solid var(--color-border);
    border-radius: 0;
    box-shadow: none;
  }

  .blog-post-card:hover,
  .portfolio-card-base:hover {
    transform: none;
    box-shadow: none;
    background: transparent;
  }
}

Production Checklist

Before deploying:

  • Build minified CSS
  • Test all pages load correctly
  • Verify responsive design (mobile, tablet, desktop)
  • Test dark mode thoroughly
  • Check accessibility (ARIA, focus states, contrast)
  • Validate HTML
  • Test with slow network
  • Verify all images have alt text
  • Test keyboard navigation
  • Check browser compatibility
  • Remove debug/development code
  • Set proper cache headers
  • Enable compression
  • Test performance (Lighthouse)

Troubleshooting

Problem: Mixin styles overriding theme

Cause: Wrong layer order

Solution:

/* Wrong: theme imported before components */
@layer theme {
  @import "custom.css";
}
@layer components {
  @import "blog.css";
}

/* Right: components before theme */
@layer components {
  @import "blog.css";
}
@layer theme {
  @import "custom.css";
}

Problem: Inconsistent styling across mixins

Cause: Different default tokens

Solution:

@layer theme {
  /* Unify all component styles */
  .blog-post-card,
  .portfolio-card-base,
  .team-member-card {
    /* Common structure */
    border-radius: var(--radius-lg);
    padding: var(--spacing-lg);
    /* ... */
  }
}

Problem: Dark mode not working everywhere

Cause: Missing dark mode token definitions

Solution:

@layer theme {
  /* Define ALL colour tokens for both modes */
  :root {
    --color-surface: white;
    --color-text-primary: black;
  }

  .dark {
    --color-surface: black; /* Don't forget! */
    --color-text-primary: white; /* Don't forget! */
  }
}

Best Practices Summary

Do's ✓

  1. Start simple - Begin with minimal customisation
  2. Use design tokens - Override tokens, not individual styles
  3. Test with multiple mixins - Ensure compatibility
  4. Document customisations - Explain why overrides exist
  5. Follow responsive patterns - Mobile-first approach
  6. Maintain accessibility - Never sacrifice for design
  7. Optimise for production - Minify, compress, cache
  8. Version your themes - Track changes properly

Don'ts ✗

  1. Don't duplicate components - Override minimally
  2. Don't hardcode values - Use tokens
  3. Don't break functionality - Only change appearance
  4. Don't ignore dark mode - Always provide dark variants
  5. Don't forget responsive - Test all screen sizes
  6. Don't skip accessibility - Follow WCAG guidelines
  7. Don't over-customise - Leverage existing components
  8. Don't deploy untested - Always test production builds

Next Steps

You now understand advanced theme development. For more information:

Summary

Key principles:

  1. Build on mixins - Don't reinvent, customise
  2. Use token system - Change appearance through variables
  3. Layer properly - Theme layer for customisation
  4. Test thoroughly - Multiple screens, modes, browsers
  5. Optimise for production - Minify, cache, compress
  6. Document everything - Future you will thank you