ReThinking Django Template Series:
Introduction
Unity first CSS framework such as Tailwind CSS, while great for rapid styling, can make your Django templates quite messy.
Its utility-first approach means you'll often have long strings of classes directly in your HTML. This verbosity clutters your templates, making them harder to read, debug, and maintain. Essentially, what you gain in speed, you often lose in code cleanliness and long-term readability.
In this blog post, I will discuss how to avoid writing long, complex CSS class names in Django templates, and how to use server side component and component CSS to make your templates cleaner and more maintainable.
Server Side Component
Below is a button which use Tailwind CSS classes to style it, code is copied from FlowBite website.
<button type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Default</button>
In many cases, developer would just copy the code in Django template directly, but this approach has several drawbacks:
- Code Duplication: Repeatedly copying the same classes across multiple templates leads to redundant code.
- Poor Readability: Long strings of Tailwind classes can make the HTML difficult to read and understand, especially for complex components.
- Lack of Reusability: The styling is tightly coupled to a specific HTML element, making it harder to reuse the button's appearance in different contexts or with different elements.
Next, I will talk about how to use component to solve these problems, I will use django-viewcomponent to demonstrate the solution, but you can also use other Django component package as well.
After installing the package, let's create components/button/button.py
from django_viewcomponent import component
@component.register("button")
class ButtonComponent(component.Component):
template_name = "button/button.html"
Create components/button/button.html
<button class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" type="button">{{ self.content }}</button>
We just created a button component, now we can use it in our Django template like this:
{% load viewcomponent_tags %}
{% component 'button' %}Default Button{% endcomponent %}
Notes:
- The css code is now encapsulated in the button component template, which makes it reusable and maintainable.
Button Variants
To create different variants of the button, we can extend the component and pass additional parameters to customize its appearance.
Edit components/button/button.py
from django_viewcomponent import component
@component.register("button")
class ButtonComponent(component.Component):
template_name = "button/button.html"
variant_map = {
"primary": "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800",
"secondary": "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800",
}
def __init__(self, variant="primary", size=None, **kwargs):
self.variant = variant
self.extra_css = kwargs.get("extra_css", "")
if self.variant and self.variant in self.variant_map:
self.extra_css += f" {self.variant_map[self.variant]}"
Edit components/button/button.html
<button class="focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none {{ self.extra_css }}" type="button">{{ self.content }}</button>
To render variants of the button, we can now use the variant
parameter:
{% load viewcomponent_tags %}
{% component 'button' variant='primary' %}Primary Button{% endcomponent %}
{% component 'button' variant='secondary' %}Secondary Button{% endcomponent %}
Notes:
- We moved some of the CSS to the component Python file, and decide which CSS classes to render based on the
variant
parameter. - The component template file also looks much cleaner since it only has common CSS classes which are used by all variants.
- Developer can also add other parameters to customize the button behavior, such as
size
,disabled
, etc.
Component CSS
Now we already know we can use server side component solution to avoid writing long, complex CSS class names in Django template, we can just render component to get things done.
In some cases, however, the css in component template might still look messy, let's take a look at below HTML.
<!-- Main modal -->
<div id="default-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-2xl max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
Terms of Service
</h3>
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="default-modal">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5 space-y-4">
<p class="text-base leading-relaxed text-gray-500 dark:text-gray-400">
With less than a month to go before the European Union enacts new consumer privacy laws for its citizens, companies around the world are updating their terms of service agreements to comply.
</p>
</div>
<!-- Modal footer -->
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
<button data-modal-hide="default-modal" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">I accept</button>
<button data-modal-hide="default-modal" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">Decline</button>
</div>
</div>
</div>
</div>
This is a modal component code copied from FlowBite website, it has a lot of CSS classes, and it is not easy to read even we put it in a component template.
In Tailwind CSS, @layer components
is a special directive, when you wrap your custom CSS classes inside @layer components { ... }, you're telling Tailwind: "Hey, these styles (.my-button, .card, etc.) are for reusable components, and they should be placed in the component layer of the final CSS output."
So take a look at the following code, we can create components/modal.css
and use @layer components
to make the modal component CSS more readable.
@layer components {
[data-controller="modal"] {
& .modal-container {
@apply overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full max-h-full;
}
& .modal-content {
@apply relative bg-white rounded-lg shadow;
}
& .modal-header {
@apply flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600;
}
& .modal-body {
@apply p-4 md:p-5 space-y-4;
}
& .modal-footer {
@apply flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600;
}
}
}
And the Modal component template can be simplified to something like this:
<div data-controller="modal">
{{ self.modal_trigger.value }}
<div data-modal-target="container" tabindex="-1" aria-hidden="true" class="hidden modal-container">
<div class="relative p-4 w-full {{ self.size_css }} max-h-full m-auto">
<div class="modal-content">
<div class="modal-header">
{{ self.modal_header.value }}
</div>
<div class="modal-body">
{{ self.modal_body.value }}
</div>
<div class="modal-footer">
{{ self.modal_footer.value }}
</div>
</div>
</div>
</div>
</div>
Notes:
- With the
modal-header
,modal-body
, andmodal-footer
CSS classes, we can use these across our application to easily generate consistent modals. - Even without server side component, we can still use this approach to simplify css in our Django templates.
So far, we have learned that we can use server side component to avoid writing long, complex CSS class names in Django template, and we can use @layer components
to make the component CSS more readable.
Next, I will talk about how to render conditional CSS class names in Django template in a more elegant way.
Conditional CSS
In some cases, we need to render css classes conditionally based on some logic and I hate writing tedious if else
statement in Django template to do this.
Server Side Component
If the behavior happen in a component, we can move the logic from component template to component class property
.
@component.register("modal")
class ModalComponent(component.Component):
dialog_map = {
"sm": "rounded-lg max-w-sm max-h-screen w-full",
"lg": "rounded-lg max-w-lg max-h-screen w-full",
"default": "rounded-lg max-w-md max-h-screen w-full",
"fullscreen": "m-0 h-full w-full max-h-full max-w-full",
}
@property
def dialog_class(self):
return self.dialog_map.get(self.size, self.dialog_map["default"])
Notes:
- In this modal component, we use
dialog_class
property to get the dialog class based on thesize
parameter, so in the component template, we can just render{{ self.dialog_class }}
to make it work, which is much cleaner than writing if else statement in the template. - If possible, putting logic in Python code can help you keep template file clean
Some Django component package allow developer to create component with a single Django template, which seems simple to use but can not handle this case very well, that is why I prefer django-viewcomponent.
yesno template filter
If you want to render different css for some variable, you can use Django yesno
template filter.
{{ some_boolean_variable|yesno:'active,inactive' }}
Classnames
In Node.js community, there is a package classnames which can help you render conditional CSS class names in a more elegant way.
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
Inspired by this package, I created django-template-simplify and Django developer can do similar things:
{% load template_simplify %}
<div class="{% class_names active=request.user.is_authenticated inactive=!request.user.is_authenticated %}"></div>
Notes:
- If user is authenticated,
active
class would be rendered. - if user
is not
authenticated,inactive
class would be rendered. !
symbol is the logicalNOT
operator here, just like Javascript.- With
class_names
, conditional CSS rendering can be better organized.
Conclusion
Tired of long Tailwind CSS classes in Django templates, this post showed how server-side components (e.g., with django-viewcomponent
) can encapsulate styles and logic, leading to cleaner, more reusable code.
We also explored component CSS using @layer components
to simplify complex class lists.
For conditional styling, we covered using component properties, Django's yesno
filter, and the class_names
utility for elegant, conditional rendering.
By implementing these strategies, you can achieve significantly cleaner and more maintainable Django templates.
ReThinking Django Template Series: