Modern web applications require sophisticated CSS and JavaScript integration to deliver rich user experiences. This chapter covers advanced techniques for organizing, processing, and optimizing CSS and JavaScript in Django applications, including preprocessors, module systems, and build workflows.
/* static/css/base.css - Base styles and CSS custom properties */
:root {
/* Color palette */
--primary-color: #3498db;
--secondary-color: #2ecc71;
--accent-color: #e74c3c;
--text-color: #2c3e50;
--text-muted: #7f8c8d;
--background-color: #ffffff;
--border-color: #ecf0f1;
/* Typography */
--font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-family-heading: 'Inter', var(--font-family-base);
--font-size-base: 16px;
--line-height-base: 1.6;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
/* Breakpoints */
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
}
/* Reset and base styles */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
font-family: var(--font-family-base);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 0;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-family-heading);
font-weight: 600;
line-height: 1.2;
margin-top: 0;
margin-bottom: var(--spacing-md);
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.75rem; }
h4 { font-size: 1.5rem; }
h5 { font-size: 1.25rem; }
h6 { font-size: 1rem; }
p {
margin-top: 0;
margin-bottom: var(--spacing-md);
}
/* Links */
a {
color: var(--primary-color);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover,
a:focus {
color: var(--accent-color);
text-decoration: underline;
}
/* static/css/components/button.css */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid transparent;
border-radius: 4px;
font-size: var(--font-size-base);
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.btn:focus {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
.btn--primary {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.btn--primary:hover {
background-color: #2980b9;
border-color: #2980b9;
}
.btn--secondary {
background-color: transparent;
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn--secondary:hover {
background-color: var(--primary-color);
color: white;
}
.btn--large {
padding: var(--spacing-md) var(--spacing-lg);
font-size: 1.125rem;
}
.btn--small {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.875rem;
}
// static/scss/variables.scss
$primary-color: #3498db;
$secondary-color: #2ecc71;
$accent-color: #e74c3c;
$font-sizes: (
'xs': 0.75rem,
'sm': 0.875rem,
'base': 1rem,
'lg': 1.125rem,
'xl': 1.25rem,
'2xl': 1.5rem,
'3xl': 1.875rem,
'4xl': 2.25rem
);
$breakpoints: (
'sm': 576px,
'md': 768px,
'lg': 992px,
'xl': 1200px
);
// Mixins
@mixin respond-to($breakpoint) {
@if map-has-key($breakpoints, $breakpoint) {
@media (min-width: map-get($breakpoints, $breakpoint)) {
@content;
}
}
}
@mixin button-variant($bg-color, $text-color: white) {
background-color: $bg-color;
color: $text-color;
border-color: $bg-color;
&:hover {
background-color: darken($bg-color, 10%);
border-color: darken($bg-color, 10%);
}
&:focus {
box-shadow: 0 0 0 3px rgba($bg-color, 0.25);
}
}
// static/scss/components/card.scss
@import '../variables';
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
&__header {
padding: 1.5rem;
border-bottom: 1px solid #f0f0f0;
h3 {
margin: 0;
font-size: map-get($font-sizes, 'xl');
}
}
&__body {
padding: 1.5rem;
}
&__footer {
padding: 1rem 1.5rem;
background: #f8f9fa;
border-top: 1px solid #f0f0f0;
}
// Responsive variations
@include respond-to('md') {
&--horizontal {
display: flex;
.card__image {
flex: 0 0 200px;
}
.card__content {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
}
// static/js/utils/api.js
class ApiClient {
constructor(baseURL = '/api/') {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: { ...this.defaultHeaders, ...options.headers },
...options
};
// Add CSRF token for non-GET requests
if (options.method && options.method !== 'GET') {
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
if (csrfToken) {
config.headers['X-CSRFToken'] = csrfToken;
}
}
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
get(endpoint, params = {}) {
const url = new URL(endpoint, this.baseURL);
Object.keys(params).forEach(key =>
url.searchParams.append(key, params[key])
);
return this.request(url.pathname + url.search);
}
post(endpoint, data = {}) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data = {}) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.request(endpoint, {
method: 'DELETE'
});
}
}
export default new ApiClient();
// static/js/components/Modal.js
export class Modal {
constructor(element, options = {}) {
this.element = element;
this.options = {
closeOnEscape: true,
closeOnBackdrop: true,
...options
};
this.isOpen = false;
this.init();
}
init() {
this.bindEvents();
this.createBackdrop();
}
bindEvents() {
// Close button
const closeBtn = this.element.querySelector('[data-modal-close]');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.close());
}
// Escape key
if (this.options.closeOnEscape) {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
});
}
// Backdrop click
if (this.options.closeOnBackdrop) {
this.element.addEventListener('click', (e) => {
if (e.target === this.element) {
this.close();
}
});
}
}
createBackdrop() {
this.element.classList.add('modal');
this.element.setAttribute('role', 'dialog');
this.element.setAttribute('aria-hidden', 'true');
}
open() {
if (this.isOpen) return;
this.isOpen = true;
document.body.classList.add('modal-open');
this.element.classList.add('modal--active');
this.element.setAttribute('aria-hidden', 'false');
// Focus management
this.previousActiveElement = document.activeElement;
const focusableElement = this.element.querySelector('[autofocus], input, button, textarea, select');
if (focusableElement) {
focusableElement.focus();
}
// Emit custom event
this.element.dispatchEvent(new CustomEvent('modal:open'));
}
close() {
if (!this.isOpen) return;
this.isOpen = false;
document.body.classList.remove('modal-open');
this.element.classList.remove('modal--active');
this.element.setAttribute('aria-hidden', 'true');
// Restore focus
if (this.previousActiveElement) {
this.previousActiveElement.focus();
}
// Emit custom event
this.element.dispatchEvent(new CustomEvent('modal:close'));
}
toggle() {
this.isOpen ? this.close() : this.open();
}
}
// static/js/components/FormValidator.js
export class FormValidator {
constructor(form, rules = {}) {
this.form = form;
this.rules = rules;
this.errors = {};
this.init();
}
init() {
this.bindEvents();
this.setupFields();
}
bindEvents() {
this.form.addEventListener('submit', (e) => {
if (!this.validate()) {
e.preventDefault();
this.showErrors();
}
});
// Real-time validation
this.form.addEventListener('blur', (e) => {
if (e.target.matches('input, textarea, select')) {
this.validateField(e.target);
}
}, true);
// Clear errors on input
this.form.addEventListener('input', (e) => {
if (e.target.matches('input, textarea, select')) {
this.clearFieldError(e.target);
}
});
}
setupFields() {
const fields = this.form.querySelectorAll('input, textarea, select');
fields.forEach(field => {
// Add ARIA attributes
field.setAttribute('aria-describedby', `${field.name}-error`);
// Create error container
const errorContainer = document.createElement('div');
errorContainer.id = `${field.name}-error`;
errorContainer.className = 'field-error';
errorContainer.setAttribute('role', 'alert');
field.parentNode.appendChild(errorContainer);
});
}
validate() {
this.errors = {};
let isValid = true;
Object.keys(this.rules).forEach(fieldName => {
const field = this.form.querySelector(`[name="${fieldName}"]`);
if (field && !this.validateField(field)) {
isValid = false;
}
});
return isValid;
}
validateField(field) {
const fieldName = field.name;
const fieldRules = this.rules[fieldName];
if (!fieldRules) return true;
const value = field.value.trim();
// Required validation
if (fieldRules.required && !value) {
this.setFieldError(field, fieldRules.messages?.required || 'This field is required');
return false;
}
// Skip other validations if field is empty and not required
if (!value && !fieldRules.required) {
this.clearFieldError(field);
return true;
}
// Email validation
if (fieldRules.email && !this.isValidEmail(value)) {
this.setFieldError(field, fieldRules.messages?.email || 'Please enter a valid email address');
return false;
}
// Min length validation
if (fieldRules.minLength && value.length < fieldRules.minLength) {
this.setFieldError(field, fieldRules.messages?.minLength || `Minimum ${fieldRules.minLength} characters required`);
return false;
}
// Custom validation
if (fieldRules.custom && !fieldRules.custom(value, field)) {
this.setFieldError(field, fieldRules.messages?.custom || 'Invalid value');
return false;
}
this.clearFieldError(field);
return true;
}
setFieldError(field, message) {
const errorContainer = document.getElementById(`${field.name}-error`);
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
}
field.classList.add('field--error');
field.setAttribute('aria-invalid', 'true');
this.errors[field.name] = message;
}
clearFieldError(field) {
const errorContainer = document.getElementById(`${field.name}-error`);
if (errorContainer) {
errorContainer.textContent = '';
errorContainer.style.display = 'none';
}
field.classList.remove('field--error');
field.setAttribute('aria-invalid', 'false');
delete this.errors[field.name];
}
showErrors() {
const firstErrorField = this.form.querySelector('.field--error');
if (firstErrorField) {
firstErrorField.focus();
}
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
<!-- templates/base.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Django App{% endblock %}</title>
<!-- Critical CSS inline -->
<style>
{% include "css/critical.css" %}
</style>
<!-- Preload important resources -->
<link rel="preload" href="{% static 'css/main.css' %}" as="style">
<link rel="preload" href="{% static 'js/main.js' %}" as="script">
<!-- Load CSS asynchronously -->
<link rel="stylesheet" href="{% static 'css/main.css' %}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="{% static 'css/main.css' %}"></noscript>
<!-- Page-specific CSS -->
{% block extra_css %}{% endblock %}
</head>
<body>
<div id="app">
{% block content %}{% endblock %}
</div>
<!-- Essential JavaScript -->
<script src="{% static 'js/main.js' %}" defer></script>
<!-- Page-specific JavaScript -->
{% block extra_js %}{% endblock %}
<!-- Initialize components -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize global components
window.app = new App();
});
</script>
</body>
</html>
<!-- templates/components/modal.html -->
<div class="modal" id="{{ modal_id }}" aria-hidden="true" role="dialog">
<div class="modal__backdrop"></div>
<div class="modal__container">
<div class="modal__header">
<h2 class="modal__title">{{ title }}</h2>
<button class="modal__close" data-modal-close aria-label="Close modal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal__body">
{{ content|safe }}
</div>
{% if actions %}
<div class="modal__footer">
{% for action in actions %}
<button class="btn btn--{{ action.type }}"
data-action="{{ action.name }}">
{{ action.label }}
</button>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const modal = new Modal(document.getElementById('{{ modal_id }}'), {
closeOnEscape: {{ close_on_escape|yesno:"true,false" }},
closeOnBackdrop: {{ close_on_backdrop|yesno:"true,false" }}
});
{% if auto_open %}
modal.open();
{% endif %}
});
</script>
# utils/css_optimizer.py
import re
import cssmin
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.files.base import ContentFile
class OptimizedCSSStorage(StaticFilesStorage):
"""Custom storage that optimizes CSS files"""
def _save(self, name, content):
if name.endswith('.css'):
# Read CSS content
css_content = content.read()
if isinstance(css_content, bytes):
css_content = css_content.decode('utf-8')
# Optimize CSS
optimized_css = self.optimize_css(css_content)
# Create new content file
content = ContentFile(optimized_css.encode('utf-8'))
return super()._save(name, content)
def optimize_css(self, css_content):
"""Optimize CSS content"""
# Remove comments
css_content = re.sub(r'/\*.*?\*/', '', css_content, flags=re.DOTALL)
# Minify CSS
css_content = cssmin.cssmin(css_content)
# Add critical CSS optimizations
css_content = self.optimize_critical_css(css_content)
return css_content
def optimize_critical_css(self, css_content):
"""Extract and optimize critical CSS"""
# This would implement critical CSS extraction logic
# For now, just return the minified CSS
return css_content
# settings.py
STATICFILES_STORAGE = 'utils.css_optimizer.OptimizedCSSStorage'
// static/js/main.js - Entry point
import { Modal } from './components/Modal.js';
import { FormValidator } from './components/FormValidator.js';
import { ApiClient } from './utils/api.js';
import { debounce, throttle } from './utils/helpers.js';
class App {
constructor() {
this.components = new Map();
this.init();
}
init() {
this.initializeComponents();
this.bindGlobalEvents();
this.setupServiceWorker();
}
initializeComponents() {
// Auto-initialize components based on data attributes
document.querySelectorAll('[data-component]').forEach(element => {
const componentName = element.dataset.component;
const componentOptions = element.dataset.options ?
JSON.parse(element.dataset.options) : {};
this.initializeComponent(element, componentName, componentOptions);
});
}
initializeComponent(element, name, options = {}) {
switch (name) {
case 'modal':
this.components.set(element, new Modal(element, options));
break;
case 'form-validator':
const rules = this.parseValidationRules(element);
this.components.set(element, new FormValidator(element, rules));
break;
default:
console.warn(`Unknown component: ${name}`);
}
}
parseValidationRules(form) {
const rules = {};
const fields = form.querySelectorAll('[data-validation]');
fields.forEach(field => {
const validationData = JSON.parse(field.dataset.validation);
rules[field.name] = validationData;
});
return rules;
}
bindGlobalEvents() {
// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'k':
e.preventDefault();
this.openSearch();
break;
}
}
});
// Global click handlers
document.addEventListener('click', (e) => {
// Handle data-action attributes
if (e.target.dataset.action) {
this.handleAction(e.target.dataset.action, e.target, e);
}
});
}
handleAction(action, element, event) {
switch (action) {
case 'toggle-theme':
this.toggleTheme();
break;
case 'copy-to-clipboard':
this.copyToClipboard(element.dataset.text || element.textContent);
break;
default:
console.warn(`Unknown action: ${action}`);
}
}
toggleTheme() {
const currentTheme = document.documentElement.dataset.theme || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.dataset.theme = newTheme;
localStorage.setItem('theme', newTheme);
}
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.showNotification('Copied to clipboard!');
} catch (err) {
console.error('Failed to copy text: ', err);
}
}
showNotification(message, type = 'info') {
// Implementation for showing notifications
const notification = document.createElement('div');
notification.className = `notification notification--${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
async setupServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration);
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
}
}
// Initialize app
window.App = App;
// static/js/utils/lazy-loader.js
export class LazyLoader {
constructor() {
this.loadedModules = new Set();
this.loadingPromises = new Map();
}
async loadComponent(componentName) {
if (this.loadedModules.has(componentName)) {
return;
}
if (this.loadingPromises.has(componentName)) {
return this.loadingPromises.get(componentName);
}
const loadPromise = this.dynamicImport(componentName);
this.loadingPromises.set(componentName, loadPromise);
try {
await loadPromise;
this.loadedModules.add(componentName);
} catch (error) {
console.error(`Failed to load component ${componentName}:`, error);
this.loadingPromises.delete(componentName);
throw error;
}
return loadPromise;
}
async dynamicImport(componentName) {
const componentMap = {
'chart': () => import('./components/Chart.js'),
'editor': () => import('./components/Editor.js'),
'calendar': () => import('./components/Calendar.js'),
'image-gallery': () => import('./components/ImageGallery.js')
};
const importFunction = componentMap[componentName];
if (!importFunction) {
throw new Error(`Unknown component: ${componentName}`);
}
return importFunction();
}
async loadCSS(href) {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
}
observeElements() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
const componentName = element.dataset.lazyComponent;
if (componentName) {
this.loadComponent(componentName).then(() => {
// Initialize component after loading
const event = new CustomEvent('component:loaded', {
detail: { componentName, element }
});
element.dispatchEvent(event);
});
observer.unobserve(element);
}
}
});
});
document.querySelectorAll('[data-lazy-component]').forEach(element => {
observer.observe(element);
});
}
}
// Initialize lazy loader
const lazyLoader = new LazyLoader();
lazyLoader.observeElements();
# utils/critical_css.py
import requests
from bs4 import BeautifulSoup
from django.conf import settings
from django.core.management.base import BaseCommand
import css_parser
import logging
class CriticalCSSExtractor:
"""Extract critical CSS for above-the-fold content"""
def __init__(self):
self.viewport_width = 1200
self.viewport_height = 800
def extract_critical_css(self, url, css_files):
"""Extract critical CSS for a given URL"""
# Get HTML content
html_content = self.fetch_html(url)
if not html_content:
return ""
# Parse HTML
soup = BeautifulSoup(html_content, 'html.parser')
# Get all CSS rules
all_css_rules = self.parse_css_files(css_files)
# Extract elements in viewport
critical_elements = self.get_critical_elements(soup)
# Match CSS rules to critical elements
critical_css = self.match_css_rules(critical_elements, all_css_rules)
return critical_css
def fetch_html(self, url):
"""Fetch HTML content from URL"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.text
except requests.RequestException as e:
logging.error(f"Failed to fetch HTML from {url}: {e}")
return None
def parse_css_files(self, css_files):
"""Parse CSS files and extract rules"""
all_rules = []
for css_file in css_files:
try:
with open(css_file, 'r', encoding='utf-8') as f:
css_content = f.read()
sheet = css_parser.parseString(css_content)
for rule in sheet:
if rule.type == rule.STYLE_RULE:
all_rules.append({
'selector': rule.selectorText,
'declarations': rule.style.cssText
})
except Exception as e:
logging.error(f"Failed to parse CSS file {css_file}: {e}")
return all_rules
def get_critical_elements(self, soup):
"""Get elements that are likely above the fold"""
critical_selectors = [
'header', 'nav', '.header', '.navbar',
'h1', 'h2', '.hero', '.banner',
'.above-fold', '[data-critical]'
]
critical_elements = set()
for selector in critical_selectors:
elements = soup.select(selector)
for element in elements:
critical_elements.add(element.name)
if element.get('class'):
for class_name in element.get('class'):
critical_elements.add(f'.{class_name}')
if element.get('id'):
critical_elements.add(f'#{element.get("id")}')
return critical_elements
def match_css_rules(self, critical_elements, css_rules):
"""Match CSS rules to critical elements"""
critical_css = []
for rule in css_rules:
selector = rule['selector']
# Simple matching - in production, use a proper CSS selector parser
if any(element in selector for element in critical_elements):
critical_css.append(f"{selector} {{ {rule['declarations']} }}")
return '\n'.join(critical_css)
# Management command to extract critical CSS
class Command(BaseCommand):
help = 'Extract critical CSS for specified URLs'
def add_arguments(self, parser):
parser.add_argument('urls', nargs='+', help='URLs to extract critical CSS for')
parser.add_argument('--css-files', nargs='+', help='CSS files to analyze')
parser.add_argument('--output', help='Output file for critical CSS')
def handle(self, *args, **options):
extractor = CriticalCSSExtractor()
all_critical_css = []
for url in options['urls']:
self.stdout.write(f'Extracting critical CSS for {url}...')
critical_css = extractor.extract_critical_css(
url,
options.get('css_files', [])
)
if critical_css:
all_critical_css.append(critical_css)
# Combine and deduplicate CSS
combined_css = '\n'.join(all_critical_css)
if options.get('output'):
with open(options['output'], 'w', encoding='utf-8') as f:
f.write(combined_css)
self.stdout.write(f'Critical CSS saved to {options["output"]}')
else:
self.stdout.write(combined_css)
// static/js/utils/feature-detection.js
export class FeatureDetector {
constructor() {
this.features = new Map();
this.detectFeatures();
}
detectFeatures() {
// CSS Features
this.features.set('css-grid', this.supportsCSSGrid());
this.features.set('css-custom-properties', this.supportsCSSCustomProperties());
this.features.set('css-flexbox', this.supportsCSSFlexbox());
// JavaScript Features
this.features.set('es6-modules', this.supportsES6Modules());
this.features.set('intersection-observer', this.supportsIntersectionObserver());
this.features.set('service-worker', this.supportsServiceWorker());
// Browser APIs
this.features.set('local-storage', this.supportsLocalStorage());
this.features.set('session-storage', this.supportsSessionStorage());
this.features.set('web-workers', this.supportsWebWorkers());
this.features.set('fetch', this.supportsFetch());
// Add feature classes to document
this.addFeatureClasses();
}
supportsCSSGrid() {
return CSS.supports('display', 'grid');
}
supportsCSSCustomProperties() {
return CSS.supports('--custom-property', 'value');
}
supportsCSSFlexbox() {
return CSS.supports('display', 'flex');
}
supportsES6Modules() {
const script = document.createElement('script');
return 'noModule' in script;
}
supportsIntersectionObserver() {
return 'IntersectionObserver' in window;
}
supportsServiceWorker() {
return 'serviceWorker' in navigator;
}
supportsLocalStorage() {
try {
const test = 'test';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
supportsSessionStorage() {
try {
const test = 'test';
sessionStorage.setItem(test, test);
sessionStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
supportsWebWorkers() {
return typeof Worker !== 'undefined';
}
supportsFetch() {
return 'fetch' in window;
}
addFeatureClasses() {
const html = document.documentElement;
this.features.forEach((supported, feature) => {
const className = supported ? `supports-${feature}` : `no-${feature}`;
html.classList.add(className);
});
}
hasFeature(feature) {
return this.features.get(feature) || false;
}
async loadPolyfill(feature, polyfillUrl) {
if (this.hasFeature(feature)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = polyfillUrl;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
}
// Initialize feature detection
const featureDetector = new FeatureDetector();
// Load polyfills as needed
(async () => {
const polyfills = [
{
feature: 'intersection-observer',
url: 'https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver'
},
{
feature: 'fetch',
url: 'https://polyfill.io/v3/polyfill.min.js?features=fetch'
}
];
for (const polyfill of polyfills) {
try {
await featureDetector.loadPolyfill(polyfill.feature, polyfill.url);
} catch (error) {
console.warn(`Failed to load polyfill for ${polyfill.feature}:`, error);
}
}
})();
export default featureDetector;
/* static/css/progressive-enhancement.css */
/* Base styles that work everywhere */
.card {
border: 1px solid #ddd;
padding: 1rem;
margin-bottom: 1rem;
}
/* Enhanced styles for modern browsers */
.supports-css-grid .card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
/* Fallback for browsers without CSS Grid */
.no-css-grid .card-grid .card {
display: inline-block;
width: 300px;
vertical-align: top;
margin-right: 1rem;
}
/* Custom properties with fallbacks */
.button {
background-color: #007bff; /* Fallback */
background-color: var(--primary-color, #007bff);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
/* Flexbox with fallbacks */
.navigation {
/* Fallback for older browsers */
text-align: center;
}
.navigation li {
display: inline-block;
margin: 0 0.5rem;
}
/* Enhanced layout for flexbox-capable browsers */
.supports-css-flexbox .navigation {
display: flex;
justify-content: space-between;
align-items: center;
text-align: left;
}
.supports-css-flexbox .navigation ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.supports-css-flexbox .navigation li {
margin: 0 1rem;
}
/* Progressive enhancement for animations */
@media (prefers-reduced-motion: no-preference) {
.supports-css-custom-properties .card {
transition: transform var(--transition-duration, 0.2s) ease;
}
.supports-css-custom-properties .card:hover {
transform: translateY(-2px);
}
}
/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
@media (prefers-color-scheme: dark) {
.supports-css-custom-properties {
--background-color: #1a1a1a;
--text-color: #ffffff;
--border-color: #333333;
}
}
With CSS and JavaScript integration mastered, you're ready to explore modern build tools that can automate and optimize your frontend workflow. The next chapter will cover integrating build tools like Vite and Webpack with Django, enabling advanced features like hot module replacement, code splitting, and automated optimization.
Key concepts covered:
These techniques provide a solid foundation for building sophisticated, performant frontend experiences while maintaining compatibility across different browsers and devices.
Working with Static Files
Django's static files system provides a robust foundation for managing CSS, JavaScript, images, and other assets in your web application. Understanding how to properly configure, organize, and serve static files is essential for building modern Django applications with rich frontend experiences.
Using Build Tools like Vite or Webpack
Modern build tools transform the frontend development experience by providing features like hot module replacement, code splitting, automatic optimization, and seamless integration with CSS preprocessors and JavaScript transpilers. This chapter covers integrating Vite and Webpack with Django to create efficient development workflows and optimized production builds.