UI Customization Guide - djadmin-filters¶
Plugin: djadmin-filters
Version: 1.0.0
Last Updated: October 31, 2025
Overview¶
The djadmin-filters plugin provides template and CSS customization options for filter widgets, sort icons, and filter sidebar layout. This guide covers how to override default templates and customize the visual appearance.
Prerequisites¶
djadmin-filtersplugin installed and configured- Familiarity with Django template system
- See Configuration Guide for setup
Template Structure¶
The plugin uses these templates:
djadmin/djadmin_filters/
├── filter_widget.html # Filter sidebar widget
└── icons/
├── sort.html # Neutral sort icon (↕)
├── sort-up.html # Ascending sort icon (↑)
└── sort-down.html # Descending sort icon (↓)
Overriding Templates¶
Django's template loader searches your app templates before plugin templates, allowing you to override any template.
Override Location¶
Place custom templates in your app's templates/ directory:
myapp/
└── templates/
└── djadmin/
└── djadmin_filters/
├── filter_widget.html
└── icons/
├── sort.html
├── sort-up.html
└── sort-down.html
Filter Widget Customization¶
Default Filter Widget¶
The default filter_widget.html template:
{# Default template structure #}
<div class="filter-sidebar">
<h3>Filters</h3>
<form method="get" action="">
{% load djadmin_tags %}
{% query_params_as_hidden_inputs 'page' filterset.form.fields %}
{% for field in filterset.form %}
<div class="filter-field">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<p class="help-text">{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="filter-submit">Apply Filters</button>
<a href="?" class="filter-clear">Clear All</a>
</form>
</div>
Custom Filter Widget¶
Create your own filter widget template:
{# myapp/templates/djadmin/djadmin_filters/filter_widget.html #}
<div class="custom-filters">
<h2 class="filters-heading">
<svg class="filter-icon"><!-- Your icon --></svg>
Filter Results
</h2>
<form method="get" action="" class="filter-form">
{% load djadmin_tags %}
{# Preserve all query params except 'page' and filter fields #}
{% query_params_as_hidden_inputs 'page' filterset.form.fields %}
<div class="filter-fields">
{% for field in filterset.form %}
<div class="filter-group">
<label for="{{ field.id_for_label }}" class="filter-label">
{{ field.label }}
</label>
<div class="filter-input">
{{ field }}
</div>
{% if field.errors %}
<div class="filter-errors">
{{ field.errors }}
</div>
{% endif %}
{% if field.help_text %}
<p class="filter-help">{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
</div>
<div class="filter-actions">
<button type="submit" class="btn btn-primary">
Apply Filters
</button>
<a href="{{ request.path }}" class="btn btn-secondary">
Clear All
</a>
</div>
</form>
</div>
Field-Specific Rendering¶
Render specific fields with custom markup:
{# myapp/templates/djadmin/djadmin_filters/filter_widget.html #}
<form method="get" action="">
{% load djadmin_tags %}
{% query_params_as_hidden_inputs 'page' filterset.form.fields %}
{# Custom rendering for specific fields #}
<div class="filter-group filter-group-search">
<label>Search Products</label>
{{ filterset.form.name }}
</div>
<div class="filter-group filter-group-price">
<label>Price Range</label>
<div class="price-range-inputs">
<div class="price-input">
<span class="currency">$</span>
{{ filterset.form.price__gte }}
</div>
<span class="range-separator">to</span>
<div class="price-input">
<span class="currency">$</span>
{{ filterset.form.price__lte }}
</div>
</div>
</div>
{# Render all other fields automatically #}
{% for field in filterset.form %}
{% if field.name not in 'name,price__gte,price__lte' %}
<div class="filter-group">
{{ field.label_tag }}
{{ field }}
</div>
{% endif %}
{% endfor %}
<button type="submit">Filter</button>
</form>
Sort Icon Customization¶
Default Sort Icons¶
The plugin includes default Unicode-based sort icons:
- Neutral: ↕ (U+2195)
- Ascending: ↑ (U+2191)
- Descending: ↓ (U+2193)
Custom SVG Icons¶
Replace with custom SVG icons:
Neutral Icon¶
{# myapp/templates/djadmin/icons/sort.html #}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
class="sort-icon sort-icon-neutral" aria-hidden="true">
<path d="M8 3l3 3H5l3-3zm0 10l-3-3h6l-3 3z" fill="currentColor"/>
</svg>
Ascending Icon¶
{# myapp/templates/djadmin/icons/sort-up.html #}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
class="sort-icon sort-icon-asc" aria-hidden="true">
<path d="M8 3l5 5H3l5-5z" fill="currentColor"/>
</svg>
Descending Icon¶
{# myapp/templates/djadmin/icons/sort-down.html #}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
class="sort-icon sort-icon-desc" aria-hidden="true">
<path d="M8 13L3 8h10l-5 5z" fill="currentColor"/>
</svg>
Font Icon Libraries¶
Use Font Awesome or similar icon libraries:
{# myapp/templates/djadmin/icons/sort.html #}
<i class="fa fa-sort" aria-hidden="true"></i>
{# myapp/templates/djadmin/icons/sort-up.html #}
<i class="fa fa-sort-up" aria-hidden="true"></i>
{# myapp/templates/djadmin/icons/sort-down.html #}
<i class="fa fa-sort-down" aria-hidden="true"></i>
CSS Customization¶
Filter Widget Styling¶
/* myapp/static/css/custom-filters.css */
/* Filter sidebar container */
.filter-sidebar {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1.5rem;
}
/* Filter heading */
.filter-sidebar h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 1rem;
color: #212529;
}
/* Filter form */
.filter-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Individual filter field */
.filter-field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
/* Filter labels */
.filter-field label {
font-size: 0.875rem;
font-weight: 500;
color: #495057;
}
/* Filter inputs */
.filter-field input,
.filter-field select {
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 0.875rem;
}
/* Filter inputs focus state */
.filter-field input:focus,
.filter-field select:focus {
outline: none;
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
/* Filter actions */
.filter-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
/* Submit button */
.filter-submit {
flex: 1;
padding: 0.5rem 1rem;
background: #0d6efd;
color: white;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
}
.filter-submit:hover {
background: #0b5ed7;
}
/* Clear button */
.filter-clear {
padding: 0.5rem 1rem;
background: transparent;
color: #6c757d;
border: 1px solid #6c757d;
border-radius: 4px;
text-decoration: none;
text-align: center;
}
.filter-clear:hover {
background: #6c757d;
color: white;
}
Sort Icon Styling¶
/* myapp/static/css/custom-sort-icons.css */
/* Sort icon container */
.sort-icon {
display: inline-block;
width: 16px;
height: 16px;
margin-left: 0.25rem;
vertical-align: middle;
transition: opacity 0.2s;
}
/* Neutral state */
.sort-icon-neutral {
opacity: 0.4;
}
/* Active states */
.sort-icon-asc,
.sort-icon-desc {
opacity: 1;
color: #0d6efd;
}
/* Sortable column header */
.sortable-column {
cursor: pointer;
user-select: none;
}
.sortable-column:hover .sort-icon-neutral {
opacity: 0.7;
}
/* Column header with active sort */
.sortable-column.sorted-asc,
.sortable-column.sorted-desc {
font-weight: 600;
}
JavaScript Customization¶
Custom Filter Behavior¶
Add custom JavaScript for filter interactions:
// myapp/static/js/custom-filters.js
document.addEventListener('DOMContentLoaded', function() {
// Auto-submit on select change
const filterSelects = document.querySelectorAll('.filter-form select');
filterSelects.forEach(select => {
select.addEventListener('change', function() {
this.form.submit();
});
});
// Clear individual filter
const filterInputs = document.querySelectorAll('.filter-field input, .filter-field select');
filterInputs.forEach(input => {
// Add clear button to each field
const clearBtn = document.createElement('button');
clearBtn.textContent = '×';
clearBtn.className = 'filter-clear-field';
clearBtn.type = 'button';
clearBtn.addEventListener('click', function() {
input.value = '';
input.form.submit();
});
input.parentNode.appendChild(clearBtn);
});
// Toggle filter sidebar on mobile
const filterToggle = document.querySelector('.filter-toggle');
const filterSidebar = document.querySelector('.filter-sidebar');
if (filterToggle && filterSidebar) {
filterToggle.addEventListener('click', function() {
filterSidebar.classList.toggle('visible');
});
}
});
Complete Customization Example¶
Custom Template¶
{# myapp/templates/djadmin/djadmin_filters/filter_widget.html #}
<!DOCTYPE html>
<aside class="custom-filter-sidebar">
<header class="filter-header">
<h2 class="filter-title">
<svg class="filter-icon" width="20" height="20">
<path d="M2 4h16M2 10h10M2 16h6" stroke="currentColor" stroke-width="2"/>
</svg>
Filters
</h2>
<button type="button" class="filter-collapse" aria-label="Collapse filters">
<svg width="20" height="20">
<path d="M5 10l5 5 5-5" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</header>
<form method="get" action="" class="filter-form" id="filter-form">
{% load djadmin_tags %}
{% query_params_as_hidden_inputs 'page' filterset.form.fields %}
<div class="filter-groups">
{% for field in filterset.form %}
{% if field.field.widget.input_type == 'select' %}
{# Dropdown filters #}
<div class="filter-group filter-group-select">
<label for="{{ field.id_for_label }}" class="filter-label">
{{ field.label }}
</label>
<div class="filter-input-wrapper">
{{ field }}
</div>
</div>
{% elif field.field.widget.input_type == 'text' %}
{# Text filters #}
<div class="filter-group filter-group-text">
<label for="{{ field.id_for_label }}" class="filter-label">
{{ field.label }}
</label>
<div class="filter-input-wrapper">
{{ field }}
<button type="button" class="clear-field" data-field="{{ field.name }}">
×
</button>
</div>
</div>
{% else %}
{# Other filters #}
<div class="filter-group">
{{ field.label_tag }}
{{ field }}
</div>
{% endif %}
{% endfor %}
</div>
<footer class="filter-footer">
<button type="submit" class="btn btn-primary btn-filter">
<svg width="16" height="16">
<path d="M15 15l-5-5m2-4a6 6 0 11-12 0 6 6 0 0112 0z"
stroke="currentColor" stroke-width="2"/>
</svg>
Apply Filters
</button>
<a href="{{ request.path }}" class="btn btn-secondary btn-clear">
Clear All
</a>
<div class="active-filter-count" data-count="{{ filterset.form.cleaned_data|length }}">
{{ filterset.form.cleaned_data|length }} active
</div>
</footer>
</form>
</aside>
Custom CSS¶
/* myapp/static/css/custom-filters.css */
.custom-filter-sidebar {
position: sticky;
top: 1rem;
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.filter-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
margin: 0;
}
.filter-icon {
color: #6b7280;
}
.filter-collapse {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
}
.filter-groups {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}
.filter-input-wrapper {
position: relative;
}
.filter-input-wrapper input,
.filter-input-wrapper select {
width: 100%;
padding: 0.625rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
transition: all 0.2s;
}
.filter-input-wrapper input:focus,
.filter-input-wrapper select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.clear-field {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
font-size: 1.25rem;
color: #9ca3af;
cursor: pointer;
padding: 0.25rem;
line-height: 1;
}
.clear-field:hover {
color: #374151;
}
.filter-footer {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.25rem;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
}
.btn {
padding: 0.625rem 1rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
text-align: center;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: white;
color: #6b7280;
border: 1px solid #d1d5db;
}
.btn-secondary:hover {
background: #f9fafb;
border-color: #9ca3af;
}
.active-filter-count {
text-align: center;
font-size: 0.75rem;
color: #6b7280;
}
.active-filter-count[data-count="0"] {
display: none;
}
/* Responsive */
@media (max-width: 768px) {
.custom-filter-sidebar {
position: fixed;
top: 0;
left: -100%;
width: 80%;
max-width: 320px;
height: 100vh;
z-index: 1000;
transition: left 0.3s;
}
.custom-filter-sidebar.visible {
left: 0;
}
}
Best Practices¶
Accessibility¶
Ensure filters are accessible:
<label for="{{ field.id_for_label }}" class="filter-label">
{{ field.label }}
</label>
{{ field }}
<button type="submit" aria-label="Apply filters">
Apply
</button>
Progressive Enhancement¶
Make filters work without JavaScript:
{# Always include a submit button #}
<button type="submit">Apply Filters</button>
{# JavaScript can add auto-submit as enhancement #}
<script>
// Auto-submit on select change (enhancement)
document.querySelectorAll('select').forEach(select => {
select.addEventListener('change', () => select.form.submit());
});
</script>
Mobile-Friendly Design¶
Use responsive design for mobile devices:
@media (max-width: 768px) {
.filter-sidebar {
position: fixed;
/* Slide-in sidebar for mobile */
}
}
See Also¶
- Configuration Guide - Plugin setup
- Filtering Guide - Filter configuration
- Ordering Guide - Sort configuration
- Advanced Usage - Complex scenarios
- Django Template Documentation - Template system reference