Component Development User Guide
Overview
This guide covers practical component development in MintyFlask. It assumes you've read the Quick Start and understand basic component usage.
What you'll learn:
- Building semantic CSS components
- Creating configurable Jinja2 macros
- Component composition patterns
- State management and variants
- Accessibility and responsive design
- Testing and documentation
Component Architecture
MintyFlask components follow a two-layer architecture:
Layer 1: CSS Components (Semantic classes)
@layer components {
.card-base {
/* styles */
}
.card-title {
/* styles */
}
.card-intent-primary {
/* modifier */
}
}
Layer 2: Macro Components (Optional orchestration)
{% macro card(title, content, intent="content") %}
<div class="card-base card-intent-{{ intent }}">
<h3 class="card-title">{{ title }}</h3>
<div class="card-body">{{ content }}</div>
</div>
{% endmacro %}
This separation allows:
- Direct CSS usage for simple cases
- Macro orchestration for complex logic
- Theme customisation at CSS level
- Consistency across all usage
Building CSS Components
Component Structure
Every CSS component follows this pattern:
@layer components {
/* Base component */
.{prefix}-{component}-base {
/* Core structural styles */
}
/* Component elements */
.{prefix}-{component}-{element} {
/* Element-specific styles */
}
/* Intent modifiers */
.{prefix}-{component}-intent-{value} {
/* Intentmodifier styles */
}
/* Size modifiers */
.{prefix}-{component}-size-{value} {
/* Size modifier styles */
}
/* State modifiers */
.{prefix}-{component}-state-{value} {
/* State modifier styles */
}
/* Dark mode */
.dark .{prefix}-{component}-base {
/* Dark mode overrides */
}
}
Example: Building a Project Card Component
Goal: Create a portfolio project card with image, title, description, and tags.
Step 1: Define base component
/* mixins/portfolio/static/css/portfolio.css */
@layer components {
/* Base card structure */
.portfolio-card-base {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.portfolio-card-base:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
/* Card elements */
.portfolio-card-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.portfolio-card-content {
padding: var(--spacing-lg);
}
.portfolio-card-title {
font-size: var(--text-xl);
font-weight: 600;
margin-bottom: var(--spacing-sm);
color: var(--color-text-primary);
}
.portfolio-card-description {
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: var(--spacing-md);
}
.portfolio-card-tags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
}
.portfolio-card-tag {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--color-surface-elevated);
border-radius: var(--radius-sm);
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
}
Step 2: Add size variants
@layer components {
/* Small variant */
.portfolio-card-size-sm .portfolio-card-content {
padding: var(--spacing-md);
}
.portfolio-card-size-sm .portfolio-card-title {
font-size: var(--text-lg);
}
/* Large variant */
.portfolio-card-size-lg .portfolio-card-content {
padding: var(--spacing-xl);
}
.portfolio-card-size-lg .portfolio-card-title {
font-size: var(--text-2xl);
}
}
Step 3: Add intent variants
@layer components {
/* Featured intent - highlighted project */
.portfolio-card-intent-featured {
border-width: 2px;
border-color: var(--color-primary);
}
.portfolio-card-intent-featured .portfolio-card-title {
color: var(--color-primary);
}
/* Client work intent - subtle styling */
.portfolio-card-intent-client {
background: var(--color-surface-elevated);
}
}
Step 4: Add dark mode
@layer components {
.dark .portfolio-card-base {
background: var(--color-surface-dark);
border-color: var(--color-border-dark);
}
.dark .portfolio-card-base:hover {
box-shadow: 0 8px 24px rgba(255, 255, 255, 0.1);
}
.dark .portfolio-card-tag {
background: var(--color-border-dark);
}
}
Step 5: Usage in template
<!-- Basic usage -->
<article class="portfolio-card-base">
<img src="project.jpg" alt="Project" class="portfolio-card-image" />
<div class="portfolio-card-content">
<h3 class="portfolio-card-title">Amazing Project</h3>
<p class="portfolio-card-description">
A beautiful web application built with React and Node.js.
</p>
<div class="portfolio-card-tags">
<span class="portfolio-card-tag">React</span>
<span class="portfolio-card-tag">Node.js</span>
</div>
</div>
</article>
<!-- Featured variant -->
<article
class="portfolio-card-base portfolio-card-intent-featured portfolio-card-size-lg"
>
<!-- Content -->
</article>
Creating Jinja2 Macros
Basic Macro Pattern
{# components/project-card.html #}
{% macro project_card(project, size="md", intent="default") %}
<article class="portfolio-card-base
{% if size %}portfolio-card-size-{{ size }}{% endif %}
{% if intent != 'default' %}portfolio-card-intent-{{ intent }}{% endif %}">
{% if project.image %}
<img src="{{ project.image }}"
alt="{{ project.title }}"
class="portfolio-card-image">
{% endif %}
<div class="portfolio-card-content">
<h3 class="portfolio-card-title">{{ project.title }}</h3>
{% if project.description %}
<p class="portfolio-card-description">{{ project.description }}</p>
{% endif %}
{% if project.tags %}
<div class="portfolio-card-tags">
{% for tag in project.tags %}
<span class="portfolio-card-tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
</article>
{% endmacro %}
Advanced Macro with Configuration
{# components/project-card.html #}
{% macro project_card(
project,
size="md",
intent="default",
show_image=true,
show_description=true,
show_tags=true,
show_link=true,
max_tags=none
) %}
<article class="portfolio-card-base
{% if size %}portfolio-card-size-{{ size }}{% endif %}
{% if intent != 'default' %}portfolio-card-intent-{{ intent }}{% endif %}"
{% if show_link and project.url %}
onclick="window.location='{{ project.url }}'"
style="cursor: pointer;"
{% endif %}>
{% if show_image and project.image %}
<img src="{{ project.image }}"
alt="{{ project.title }}"
class="portfolio-card-image"
loading="lazy">
{% endif %}
<div class="portfolio-card-content">
<h3 class="portfolio-card-title">
{% if show_link and project.url %}
<a href="{{ project.url }}" class="hover:underline">
{{ project.title }}
</a>
{% else %}
{{ project.title }}
{% endif %}
</h3>
{% if show_description and project.description %}
<p class="portfolio-card-description">
{{ project.description }}
</p>
{% endif %}
{% if show_tags and project.tags %}
<div class="portfolio-card-tags">
{% set displayed_tags = project.tags[:max_tags] if max_tags else project.tags %}
{% for tag in displayed_tags %}
<span class="portfolio-card-tag">{{ tag }}</span>
{% endfor %}
{% if max_tags and project.tags|length > max_tags %}
<span class="portfolio-card-tag">
+{{ project.tags|length - max_tags }} more
</span>
{% endif %}
</div>
{% endif %}
</div>
</article>
{% endmacro %}
Using the Macro
{# In your template #}
{% from "components/project-card.html" import project_card with context %}
{# Basic usage #}
{{ project_card(project) }}
{# Featured large card #}
{{ project_card(project, size="lg", intent="featured") }}
{# Card without image, limited tags #}
{{ project_card(
project,
show_image=false,
max_tags=3
) }}
{# Grid of projects #}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for project in projects %}
{{ project_card(project, size="sm") }}
{% endfor %}
</div>
Component Composition
Pattern 1: Macro Calling Macro
{# components/cards.html #}
{% macro simple_card(title, content) %}
<div class="card-base card-intent-content">
<div class="card-body">
<h3 class="card-title">{{ title }}</h3>
<p class="card-description">{{ content }}</p>
</div>
</div>
{% endmacro %}
{% macro info_card(title, content, icon=none) %}
<div class="card-base card-intent-info">
<div class="card-header">
{% if icon %}
<div class="text-2xl mb-2">{{ icon }}</div>
{% endif %}
<h3 class="card-title">{{ title }}</h3>
</div>
<div class="card-body">
<p class="card-description">{{ content }}</p>
</div>
</div>
{% endmacro %}
{% macro feature_grid(features) %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{% for feature in features %}
{{ info_card(feature.title, feature.description, feature.icon) }}
{% endfor %}
</div>
{% endmacro %}
Pattern 2: Partial with Macro
{# partials/post-card.html #}
{% from "blog-components.html" import
post_metadata_enhanced,
post_tag_cloud
with context %}
<article class="blog-post-card blog-post-card-size-{{ card_size|default('md') }}">
<div class="blog-post-header">
{{ post_metadata_enhanced(post, show_reading_time=true) }}
</div>
<div class="blog-post-body">
<h2 class="blog-post-title">
<a href="{{ url_for('blog_bp.post_view', slug=post.slug) }}">
{{ post.meta.title }}
</a>
</h2>
{% if post.meta.summary %}
<p class="blog-post-excerpt">{{ post.meta.summary }}</p>
{% endif %}
{{ post_tag_cloud(post) }}
</div>
</article>
Pattern 3: Layout with Slots
{# layouts/card-layout.html #}
{% macro card_layout(
title,
intent="content",
size="md",
show_header=true,
show_footer=true
) %}
<div class="card-base card-intent-{{ intent }} card-size-{{ size }}">
{% if show_header and title %}
<div class="card-header">
<h3 class="card-title">{{ title }}</h3>
{% if caller.header is defined %}
{{ caller.header() }}
{% endif %}
</div>
{% endif %}
<div class="card-body">
{{ caller() }}
</div>
{% if show_footer and caller.footer is defined %}
<div class="card-footer">
{{ caller.footer() }}
</div>
{% endif %}
</div>
{% endmacro %}
Usage with callable blocks:
{% from "layouts/card-layout.html" import card_layout %}
{% call card_layout(title="My Card", intent="feature") %}
<p>This is the main content of the card.</p>
<p>It can be multiple paragraphs.</p>
{% call (header=true) %}
<span class="badge-base badge-intent-info">New</span>
{% endcall %}
{% call (footer=true) %}
<button class="btn-base btn-intent-primary">Action</button>
{% endcall %}
{% endcall %}
Responsive Components
Mobile-First Approach
@layer components {
/* Mobile (default) */
.portfolio-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
.portfolio-card-title {
font-size: var(--text-lg);
}
/* Tablet */
@media (min-width: 640px) {
.portfolio-grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-lg);
}
.portfolio-card-title {
font-size: var(--text-xl);
}
}
/* Desktop */
@media (min-width: 1024px) {
.portfolio-grid {
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-xl);
}
.portfolio-card-title {
font-size: var(--text-2xl);
}
}
}
Responsive Variants
@layer components {
/* Stack on mobile, row on desktop */
.feature-card {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
@media (min-width: 768px) {
.feature-card {
flex-direction: row;
align-items: centre;
}
.feature-card-image {
width: 40%;
}
.feature-card-content {
width: 60%;
}
}
}
Accessibility
Semantic HTML
{% macro accessible_card(title, content, role="article") %}
<article class="card-base"
role="{{ role }}"
{% if title %}aria-labelledby="card-title-{{ loop.index }}"{% endif %}>
{% if title %}
<div class="card-header">
<h3 class="card-title" id="card-title-{{ loop.index }}">
{{ title }}
</h3>
</div>
{% endif %}
<div class="card-body">
{{ content }}
</div>
</article>
{% endmacro %}
Focus States
@layer components {
.portfolio-card-base:focus-within {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.portfolio-card-title a:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
text-decoration: underline;
}
}
ARIA Labels
{% macro button_with_icon(text, icon, label=none) %}
<button class="btn-base btn-intent-primary btn-icon-left"
aria-label="{{ label or text }}">
<svg aria-hidden="true">{{ icon }}</svg>
<span>{{ text }}</span>
</button>
{% endmacro %}
Testing Components
Visual Testing
<!-- components/tests/visual-test.html -->
<div class="p-8 space-y-8">
<h2>Component Visual Tests</h2>
<!-- Test all variants -->
<section>
<h3>Size Variants</h3>
<div class="flex gap-4">
{{ project_card(sample_project, size="sm") }}
{{ project_card(sample_project, size="md") }}
{{ project_card(sample_project, size="lg") }}
</div>
</section>
<section>
<h3>Intent Variants</h3>
<div class="flex gap-4">
{{ project_card(sample_project, intent="default") }}
{{ project_card(sample_project, intent="featured") }}
{{ project_card(sample_project, intent="client") }}
</div>
</section>
<section>
<h3>Dark Mode</h3>
<div class="dark p-4 bg-slate-900">
{{ project_card(sample_project) }}
</div>
</section>
</section>
Component Documentation
{# components/project-card.html #}
{#
# Portfolio Project Card Component
#
# Displays a portfolio project with image, title, description, and tags
#
# @param project: Project object (required)
# - project.title: Project title (required)
# - project.description: Project description (optional)
# - project.image: Project image URL (optional)
# - project.tags: Array of tag strings (optional)
# - project.url: Project URL (optional)
#
# @param size: Card size - "sm", "md", "lg" (default: "md")
# @param intent: Card intent - "default", "featured", "client" (default: "default")
# @param show_image: Display project image (default: true)
# @param show_description: Display project description (default: true)
# @param show_tags: Display project tags (default: true)
# @param show_link: Make card clickable if project.url exists (default: true)
# @param max_tags: Maximum number of tags to display (default: none - show all)
#
# Example usage:
# {{ project_card(project) }}
# {{ project_card(project, size="lg", intent="featured") }}
# {{ project_card(project, show_image=false, max_tags=3) }}
#}
{% macro project_card(...) %}
<!-- Component code -->
{% endmacro %}
Best Practices
CSS Component Guidelines
Do:
- ✓ Use semantic class names
- ✓ Prefix with mixin name
- ✓ Use CSS custom properties
- ✓ Include dark mode variants
- ✓ Follow BEM-style modifiers
- ✓ Keep components focused
Don't:
- ✗ Hardcode values
- ✗ Use overly specific selectors
- ✗ Create generic names without prefix
- ✗ Duplicate entire components for variants
- ✗ Break accessibility
Macro Component Guidelines
Do:
- ✓ Provide sensible defaults
- ✓ Make parameters optional
- ✓ Document parameters
- ✓ Use semantic HTML
- ✓ Include ARIA labels
- ✓ Add
with contextfor Flask functions
Don't:
- ✗ Create macros for simple CSS
- ✗ Embed complex logic
- ✗ Hardcode URLs or values
- ✗ Create deep nesting
- ✗ Forget error handling
Next Steps
You now understand how to build comprehensive components. For more information:
- CSS Architecture User Guide - Layer system and styling
- Theme Creation Guide - Using components in themes
Summary
Key principles:
- Semantic CSS first - Build with meaningful classes
- Macros for logic - Add macros when needed for configuration
- Component composition - Combine small pieces
- Accessibility always - Semantic HTML, ARIA, focus states
- Test thoroughly - Visual tests, dark mode, responsive
- Document well - Clear parameter descriptions
With these practices, you'll build maintainable, reusable components that work across your entire site.