-
Notifications
You must be signed in to change notification settings - Fork 2
Closed
Description
Code of Conduct
- I agree to follow Django's Code of Conduct
Feature Description
Overview
Implementation of a native ToggleSwitchWidget for Django forms framework, providing a modern and accessible toggle switch component as an enhanced alternative to the traditional checkbox input.
Problem
Problem Statement
Currently, Django developers need to:
- Create custom implementations for toggle switches
- Rely on third-party packages with varying quality and maintenance
- Use basic checkboxes where toggle switches would provide better UX
- Implement accessibility features manually each time
Request or proposal
proposal
Additional Details
Proposed Solution
A built-in ToggleSwitchWidget that:
- Extends
CheckboxInputmaintaining full backward compatibility - Provides customizable visual properties (size, colors, labels)
- Ensures WCAG 2.1 AA accessibility compliance
- Requires zero external dependencies
- Supports all modern browsers
Implementation Suggestions
use:
.......
widgets = {
......
}),
'solicitada_mpu': ToggleSwitchWidget(size='sm',
active_color='#9333ea',
inactive_color='#ccc',
active_text='MPU Solicitada',
inactive_text='Não Solicitada'
),
...............
django_toggle_switch.py
from django.forms.widgets import CheckboxInput
from django.utils.safestring import mark_safe
from django.utils.html import format_html
import json
import uuid
class ToggleSwitchWidget(CheckboxInput):
"""
A customizable toggle switch widget for Django forms.
Author: Ogliari Natan
Date: 11-08-2025
Proposal for Django Foundation
Args:
size (str): Size of the toggle - 'xs', 'sm', 'md', 'lg', 'xl' or custom tuple (width, height)
active_color (str): Color when toggle is active (default: '#10B981')
inactive_color (str): Color when toggle is inactive (default: '#9CA3AF')
active_text (str): Text to display when active (default: 'Active')
inactive_text (str): Text to display when inactive (default: 'Inactive')
show_label (bool): Whether to show the text label (default: True)
animation_speed (float): Animation speed in seconds (default: 0.3)
"""
# Predefined sizes with maintained proportions
SIZES = {
'xs': (32, 16), # width, height in pixels
'sm': (40, 20),
'md': (48, 24),
'lg': (56, 28),
'xl': (64, 32),
}
def __init__(self, size='md', active_color="#B910B9", inactive_color='#9CA3AF',
active_text='Active', inactive_text='Inactive', show_label=True,
animation_speed=0.3, attrs=None):
super().__init__(attrs)
# Handle size parameter
if isinstance(size, str) and size in self.SIZES:
self.width, self.height = self.SIZES[size]
elif isinstance(size, (tuple, list)) and len(size) == 2:
self.width, self.height = size
else:
self.width, self.height = self.SIZES['md']
# Calculate proportional dimensions
self.button_size = int(self.height * 0.75) # Button is 75% of height
self.padding = int(self.height * 0.125) # Padding is 12.5% of height
self.translate_x = self.width - self.button_size - (self.padding * 2)
# Set colors and text
self.active_color = active_color
self.inactive_color = inactive_color
self.active_text = active_text
self.inactive_text = inactive_text
self.show_label = show_label
self.animation_speed = animation_speed
def render(self, name, value, attrs=None, renderer=None):
if attrs is None:
attrs = {}
# Generate unique ID for this instance
widget_id = attrs.get('id', f'toggle_{uuid.uuid4().hex[:8]}')
is_checked = bool(value)
checked_attr = 'checked' if is_checked else ''
# Build the widget HTML
context = {
'widget_id': widget_id,
'name': name,
'checked': checked_attr,
'width': self.width,
'height': self.height,
'button_size': self.button_size,
'padding': self.padding,
'translate_x': self.translate_x,
'active_color': self.active_color,
'inactive_color': self.inactive_color,
'active_text': self.active_text,
'inactive_text': self.inactive_text,
'current_text': self.active_text if is_checked else self.inactive_text,
'current_color': self.active_color if is_checked else self.inactive_color,
'show_label': 'inline-block' if self.show_label else 'none',
'animation_speed': self.animation_speed,
'font_size': max(12, int(self.height * 0.6)), # Proportional font size
}
html = '''
<div class="django-toggle-switch-wrapper" id="wrapper_%(widget_id)s">
<style>
#wrapper_%(widget_id)s {
display: inline-flex;
align-items: center;
gap: 8px;
}
#wrapper_%(widget_id)s .django-toggle-switch {
position: relative;
display: inline-block;
width: %(width)spx;
height: %(height)spx;
cursor: pointer;
}
#wrapper_%(widget_id)s .django-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
#wrapper_%(widget_id)s .django-toggle-slider {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: %(inactive_color)s;
transition: background-color %(animation_speed)ss;
border-radius: %(height)spx;
}
#wrapper_%(widget_id)s .django-toggle-slider:before {
position: absolute;
content: "";
height: %(button_size)spx;
width: %(button_size)spx;
left: %(padding)spx;
bottom: %(padding)spx;
background-color: white;
transition: transform %(animation_speed)ss;
border-radius: 50%%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
#wrapper_%(widget_id)s input:checked + .django-toggle-slider {
background-color: %(active_color)s;
}
#wrapper_%(widget_id)s input:checked + .django-toggle-slider:before {
transform: translateX(%(translate_x)spx);
}
#wrapper_%(widget_id)s input:focus + .django-toggle-slider {
box-shadow: 0 0 0 2px rgba(0,0,0,0.1);
}
#wrapper_%(widget_id)s .django-toggle-label {
display: %(show_label)s;
font-size: %(font_size)spx;
user-select: none;
transition: color %(animation_speed)ss;
color: %(current_color)s;
}
/* Accessibility improvements */
#wrapper_%(widget_id)s input:focus-visible + .django-toggle-slider {
outline: 2px solid %(active_color)s;
outline-offset: 2px;
}
/* Hover effect */
#wrapper_%(widget_id)s .django-toggle-switch:hover .django-toggle-slider {
opacity: 0.8;
}
/* Disabled state */
#wrapper_%(widget_id)s input:disabled + .django-toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<label class="django-toggle-switch" for="%(widget_id)s">
<input type="checkbox"
name="%(name)s"
id="%(widget_id)s"
%(checked)s
aria-checked="%(checked)s"
aria-label="%(current_text)s"
role="switch">
<span class="django-toggle-slider"></span>
</label>
<span class="django-toggle-label" id="%(widget_id)s_label">
%(current_text)s
</span>
<script>
(function() {
const toggle = document.getElementById('%(widget_id)s');
const label = document.getElementById('%(widget_id)s_label');
const config = {
activeText: '%(active_text)s',
inactiveText: '%(inactive_text)s',
activeColor: '%(active_color)s',
inactiveColor: '%(inactive_color)s'
};
if (toggle && label) {
toggle.addEventListener('change', function() {
if (this.checked) {
label.textContent = config.activeText;
label.style.color = config.activeColor;
this.setAttribute('aria-checked', 'true');
this.setAttribute('aria-label', config.activeText);
} else {
label.textContent = config.inactiveText;
label.style.color = config.inactiveColor;
this.setAttribute('aria-checked', 'false');
this.setAttribute('aria-label', config.inactiveText);
}
});
}
})();
</script>
</div>
''' % context
return mark_safe(html)
OgliariNatanshangxiao and benbacardiandrew-cybsafe, DmytroLitvinov, OgliariNatan and VaZark
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Status
Done