Skip to content

Renderer Customization Guide - djadmin-formset

Plugin: djadmin-formset Version: 0.1.0 (Alpha) Last Updated: October 21, 2025

Overview

The djadmin-formset plugin provides DjAdminFormRenderer, a custom renderer for django-formset that integrates seamlessly with the djadmin Tailwind CSS theme. You can customize the renderer or create your own to match your design system.

Default Renderer: DjAdminFormRenderer

Features

The DjAdminFormRenderer class provides:

  • Tailwind CSS Integration: Matches djadmin's Tailwind-based theme
  • Customizable CSS Classes: Override classes for all form elements
  • Fieldset Support: Handles both named and unnamed fieldsets
  • Collection Rendering: Styled inline forms with add/remove buttons
  • Responsive Design: Mobile-friendly form layouts

Default CSS Classes

# djadmin_formset/renderers.py

class DjAdminFormRenderer(FormRenderer):
    """
    Custom renderer for django-formset with djadmin theme integration.

    Provides Tailwind CSS classes matching the djadmin design system.
    """

    # Field wrapper
    field_css_classes = 'mb-4'

    # Label element
    label_css_classes = 'block text-sm font-medium text-gray-700 mb-1'

    # Input fields
    field_input_css_classes = (
        'w-full px-3 py-2 border border-gray-300 rounded-md '
        'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
    )

    # Textarea
    textarea_css_classes = (
        'w-full px-3 py-2 border border-gray-300 rounded-md '
        'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
    )

    # Select dropdown
    select_css_classes = (
        'w-full px-3 py-2 border border-gray-300 rounded-md '
        'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
    )

    # Checkbox
    checkbox_css_classes = 'h-4 w-4 text-blue-600 border-gray-300 rounded'

    # Help text
    help_text_css_classes = 'mt-1 text-sm text-gray-500'

    # Error messages
    error_css_classes = 'mt-1 text-sm text-red-600'

    # Fieldset
    fieldset_css_classes = 'mb-6 p-4 border border-gray-300 rounded-lg'

    # Fieldset legend
    legend_css_classes = 'text-lg font-semibold text-gray-900 px-2'

    # Collection wrapper
    collection_css_classes = 'space-y-4'

    # Collection item
    collection_item_css_classes = (
        'p-4 border border-gray-200 rounded-lg bg-gray-50 relative'
    )

    # Add/remove buttons
    button_css_classes = (
        'px-4 py-2 bg-blue-600 text-white rounded-md '
        'hover:bg-blue-700 focus:outline-none focus:ring-2 '
        'focus:ring-blue-500'
    )

Basic Usage

Use Default Renderer

The default renderer is applied automatically to all forms:

from djadmin import ModelAdmin, register, Layout, Field

@register(Author)
class AuthorAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Field('email'),
    )
    # DjAdminFormRenderer is used automatically

Explicit Renderer

You can explicitly set the renderer on a layout:

from djadmin import ModelAdmin, register, Layout, Field
from djadmin_formset.renderers import DjAdminFormRenderer

@register(Author)
class AuthorAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Field('email'),
        renderer=DjAdminFormRenderer,  # Explicit
    )

Customizing CSS Classes

Option 1: Override at Instantiation

Pass custom CSS classes when creating a renderer instance:

from djadmin import ModelAdmin, register, Layout, Field
from djadmin_formset.renderers import DjAdminFormRenderer

@register(Author)
class AuthorAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Field('email'),
        renderer=DjAdminFormRenderer(
            # Override specific classes
            field_css_classes='mb-6',  # More spacing
            label_css_classes='block text-base font-bold text-gray-800 mb-2',
            field_input_css_classes='w-full px-4 py-3 border-2 border-blue-500 rounded-lg',
        ),
    )

Option 2: Create Custom Renderer Subclass

For project-wide customization, create a custom renderer:

# myapp/renderers.py

from djadmin_formset.renderers import DjAdminFormRenderer

class MyCustomRenderer(DjAdminFormRenderer):
    """Custom renderer with Bootstrap 5 classes."""

    # Override CSS classes
    field_input_css_classes = 'form-control'
    textarea_css_classes = 'form-control'
    select_css_classes = 'form-select'
    checkbox_css_classes = 'form-check-input'
    label_css_classes = 'form-label'
    help_text_css_classes = 'form-text text-muted'
    error_css_classes = 'invalid-feedback'
    fieldset_css_classes = 'border p-3 rounded mb-4'
    legend_css_classes = 'h5 mb-3'

Use it in your admin:

from djadmin import ModelAdmin, register, Layout, Field
from myapp.renderers import MyCustomRenderer

@register(Author)
class AuthorAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Field('email'),
        renderer=MyCustomRenderer,
    )

Option 3: Settings-Based Configuration

Override the default renderer globally via Django settings:

# settings.py

DJADMIN_FORMSET_DEFAULT_RENDERER = 'myapp.renderers.MyCustomRenderer'

Then use the helper to get the configured renderer:

from djadmin import ModelAdmin, register, Layout, Field
from djadmin_formset.renderers import get_default_renderer

@register(Author)
class AuthorAdmin(ModelAdmin):
    layout = Layout(
        Field('name'),
        Field('email'),
        renderer=get_default_renderer(),  # Uses setting
    )

Advanced Customization

Customizing Rendering Methods

Override rendering methods for complete control:

# myapp/renderers.py

from djadmin_formset.renderers import DjAdminFormRenderer

class MyAdvancedRenderer(DjAdminFormRenderer):
    """Advanced renderer with custom rendering logic."""

    def _amend_fieldset(self, context):
        """Customize fieldset rendering."""
        # Call parent implementation
        context = super()._amend_fieldset(context)

        # Add custom context
        context['show_description'] = True
        context['collapse_by_default'] = False

        return context

    def _amend_form(self, context):
        """Customize form rendering."""
        context = super()._amend_form(context)

        # Add custom attributes
        context['form_attrs'] = {
            'data-form-type': 'djadmin',
            'data-theme': 'custom',
        }

        return context

    def _amend_collection(self, context):
        """Customize collection rendering."""
        context = super()._amend_collection(context)

        # Custom collection settings
        context['allow_empty'] = True
        context['max_items'] = 20

        return context

Template Customization

Override the default templates used by django-formset:

# settings.py

DJADMIN_FORMSET_TEMPLATES = {
    'help_text': 'myapp/formset/help_text.html',
    'field': 'myapp/formset/field.html',
    'fieldset': 'myapp/formset/fieldset.html',
}

The renderer will use these templates:

from django.conf import settings
from formset.renderers import FormRenderer

class DjAdminFormRenderer(FormRenderer):
    """Uses custom templates from settings."""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        # Override templates from settings
        custom_templates = getattr(settings, 'DJADMIN_FORMSET_TEMPLATES', {})
        if 'help_text' in custom_templates:
            self.templates['help_text'] = custom_templates['help_text']
        # ... similar for other templates

Example: Material Design Renderer

# myapp/renderers.py

from djadmin_formset.renderers import DjAdminFormRenderer

class MaterialRenderer(DjAdminFormRenderer):
    """Renderer using Material Design Lite classes."""

    field_css_classes = 'mdl-textfield mdl-js-textfield mdl-textfield--floating-label'
    label_css_classes = 'mdl-textfield__label'
    field_input_css_classes = 'mdl-textfield__input'
    error_css_classes = 'mdl-textfield__error'
    button_css_classes = 'mdl-button mdl-js-button mdl-button--raised mdl-button--colored'
    fieldset_css_classes = 'mdl-card mdl-shadow--2dp'
    legend_css_classes = 'mdl-card__title-text'

    def _amend_form(self, context):
        """Add Material Design form wrapper."""
        context = super()._amend_form(context)
        context['form_wrapper_classes'] = 'mdl-grid'
        return context

CSS Class Reference

Field Elements

Element Attribute Default
Field wrapper field_css_classes 'mb-4'
Label label_css_classes 'block text-sm font-medium text-gray-700 mb-1'
Text input field_input_css_classes 'w-full px-3 py-2 border ...'
Textarea textarea_css_classes 'w-full px-3 py-2 border ...'
Select select_css_classes 'w-full px-3 py-2 border ...'
Checkbox checkbox_css_classes 'h-4 w-4 text-blue-600 ...'
Help text help_text_css_classes 'mt-1 text-sm text-gray-500'
Error message error_css_classes 'mt-1 text-sm text-red-600'

Layout Elements

Element Attribute Default
Fieldset fieldset_css_classes 'mb-6 p-4 border ...'
Legend legend_css_classes 'text-lg font-semibold ...'
Collection wrapper collection_css_classes 'space-y-4'
Collection item collection_item_css_classes 'p-4 border border-gray-200 ...'
Buttons button_css_classes 'px-4 py-2 bg-blue-600 ...'

Renderer Method Reference

__init__(**kwargs)

Constructor accepting CSS class overrides:

renderer = DjAdminFormRenderer(
    field_css_classes='custom-field',
    label_css_classes='custom-label',
)

_amend_fieldset(context)

Called when rendering fieldsets. Override to customize fieldset behavior:

def _amend_fieldset(self, context):
    context = super()._amend_fieldset(context)
    # Handle legend=None for unnamed fieldsets
    if context.get('legend') is None:
        context['show_legend'] = False
    return context

_amend_form(context)

Called when rendering forms. Override to add form-level customization:

def _amend_form(self, context):
    context = super()._amend_form(context)
    context['form_classes'] = 'my-custom-form'
    return context

_amend_collection(context)

Called when rendering collections. Override for collection customization:

def _amend_collection(self, context):
    context = super()._amend_collection(context)
    context['allow_sorting'] = True
    context['show_add_button'] = True
    return context

Best Practices

1. Use Inheritance

Don't create renderers from scratch - inherit from DjAdminFormRenderer:

# Good
class MyRenderer(DjAdminFormRenderer):
    field_css_classes = 'custom-field'

# Avoid (unless you really need to)
class MyRenderer(FormRenderer):
    # Have to implement everything yourself

2. Keep CSS Classes Consistent

Use a consistent design system throughout your renderer:

class ConsistentRenderer(DjAdminFormRenderer):
    """All elements use the same spacing unit (rem)."""

    field_css_classes = 'mb-4'      # 1rem spacing
    fieldset_css_classes = 'mb-6'   # 1.5rem spacing
    collection_css_classes = 'mb-8' # 2rem spacing

3. Test Your Renderer

Create a test admin with various field types:

# tests/test_custom_renderer.py

from myapp.renderers import MyCustomRenderer

class RendererTestAdmin(ModelAdmin):
    layout = Layout(
        Field('text_field'),
        Field('textarea_field', widget='textarea'),
        Field('select_field'),
        Field('checkbox_field'),
        Fieldset('Group', Field('nested_field')),
        renderer=MyCustomRenderer,
    )

4. Document Custom Classes

If creating a custom renderer, document the CSS classes:

class MyRenderer(DjAdminFormRenderer):
    """
    Custom renderer using Bootstrap 5.

    CSS Classes:
    - field_input_css_classes: 'form-control' (Bootstrap form control)
    - label_css_classes: 'form-label' (Bootstrap label)
    - error_css_classes: 'invalid-feedback' (Bootstrap error)
    """

Troubleshooting

Styles Not Applied

Symptom: Custom CSS classes not appearing in HTML

Check: 1. Renderer correctly specified in layout? 2. Template caching enabled? Clear cache: python manage.py clear_cache 3. Static files collected? Run: python manage.py collectstatic

Conflicts with Core Styles

Symptom: djadmin core styles conflict with renderer styles

Solution: Use more specific selectors or override in your CSS:

/* myapp/static/css/custom.css */

/* Override djadmin styles for formset forms */
form[data-formset] .custom-field {
    /* Your custom styles */
}

Renderer Not Found

Symptom: ImportError: cannot import name 'MyRenderer'

Solution: Check import path in layout:

# Wrong
from myapp.renderers import MyRenderer  # File not in path

# Correct
from myapp.renderers import MyRenderer  # Ensure myapp is in INSTALLED_APPS

Examples

Example 1: Dark Mode Renderer

class DarkModeRenderer(DjAdminFormRenderer):
    """Dark mode theme renderer."""

    field_input_css_classes = (
        'w-full px-3 py-2 bg-gray-800 text-white border border-gray-600 rounded-md '
        'focus:outline-none focus:ring-2 focus:ring-blue-400'
    )
    label_css_classes = 'block text-sm font-medium text-gray-300 mb-1'
    fieldset_css_classes = 'mb-6 p-4 border border-gray-600 rounded-lg bg-gray-900'
    legend_css_classes = 'text-lg font-semibold text-gray-100 px-2'

Example 2: Minimal Renderer

class MinimalRenderer(DjAdminFormRenderer):
    """Minimal, clean renderer with less visual weight."""

    field_css_classes = 'mb-3'
    label_css_classes = 'text-xs font-normal text-gray-600 mb-1'
    field_input_css_classes = (
        'w-full px-2 py-1 border-b border-gray-300 '
        'focus:outline-none focus:border-blue-500'
    )
    fieldset_css_classes = 'mb-4'
    legend_css_classes = 'text-sm font-medium text-gray-700 mb-2'

Example 3: Accessibility-First Renderer

class AccessibleRenderer(DjAdminFormRenderer):
    """Enhanced accessibility with ARIA attributes."""

    def _amend_form(self, context):
        context = super()._amend_form(context)
        context['form_attrs'] = {
            'role': 'form',
            'aria-labelledby': 'form-title',
        }
        return context

    def _amend_fieldset(self, context):
        context = super()._amend_fieldset(context)
        if context.get('legend'):
            context['fieldset_attrs'] = {
                'role': 'group',
                'aria-labelledby': f"legend-{context['legend'].lower().replace(' ', '-')}",
            }
        return context

See Also