🎨 Developer Cookbook - FASE 7: UX/UI para Desarrolladores
Principios de diseño y usabilidad para crear interfaces que deleitan
📚 Tabla de Contenidos
- Receta 7.6: Principios de Diseño Centrado en el Usuario
- Receta 7.7: Psicología del Usuario - Cognitive Load
- Receta 7.8: Prototipado Rápido con Figma
- Receta 7.9: Accesibilidad (WCAG)
Receta 7.6: Principios de Diseño Centrado en el Usuario
¿Qué es? Diseñar productos pensando en el usuario real, no en lo que asumimos que necesitan.
5 Principios fundamentales:
class UXPrinciples:
"""Principios de UX que todo developer debe conocer"""
@staticmethod
def principle_1_visibility():
"""Visibility of System Status"""
print("1️⃣ VISIBILITY OF SYSTEM STATUS")
print(" El usuario siempre debe saber qué está pasando\n")
print(" ❌ MAL:")
print(" [Submit button] → *nada pasa por 3 segundos* → Success!")
print("\n ✅ BIEN:")
print(" [Submit button] → [Loading spinner] → [Progress: 50%] → Success!")
code = '''
# Ejemplo en código
def upload_file(file):
# ❌ MAL: No feedback
process_file(file)
return "Done"
# ✅ BIEN: Progress feedback
total_steps = 5
update_progress(0, total_steps, "Uploading...")
upload_to_s3(file)
update_progress(1, total_steps, "Scanning for viruses...")
scan_file(file)
update_progress(2, total_steps, "Generating thumbnail...")
create_thumbnail(file)
update_progress(3, total_steps, "Updating database...")
save_to_db(file)
update_progress(4, total_steps, "Complete!")
return "Done"
'''
print(f"\n{code}\n")
@staticmethod
def principle_2_real_world():
"""Match Between System and Real World"""
print("2️⃣ MATCH BETWEEN SYSTEM AND REAL WORLD")
print(" Usa lenguaje que los usuarios entienden, no jerga técnica\n")
print(" ❌ MAL (Developer language):")
print(" 'Error 429: Rate limit exceeded. Retry after exponential backoff'")
print("\n ✅ BIEN (Human language):")
print(" 'Whoa, slow down! You are making too many requests. Try again in 1 minute.'")
print("\n ❌ MAL (Buttons):")
print(" [Confirm] [Cancel]")
print("\n ✅ BIEN (Descriptive):")
print(" [Delete 23 files] [Keep files]")
code = '''
# Mensajes de error
errors = {
# ❌ Developer-speak
"bad": "HTTP 400 Bad Request: Invalid JSON payload",
# ✅ Human-speak
"good": "Oops! We couldn't understand your request. Please check your input."
}
# Confirmaciones
confirmations = {
# ❌ Generic
"bad": "Are you sure?",
# ✅ Specific
"good": "Delete 156 customer records? This cannot be undone."
}
'''
print(f"{code}\n")
@staticmethod
def principle_3_user_control():
"""User Control and Freedom"""
print("3️⃣ USER CONTROL AND FREEDOM")
print(" Los usuarios cometen errores. Dale 'undo' fácil\n")
print(" ✅ Gmail: 'Undo send' por 5 segundos")
print(" ✅ Slack: Edit/delete messages")
print(" ✅ Stripe: Refund con 1 click")
code = '''
# Pattern: Soft delete con undo
class TaskManager:
def __init__(self):
self.tasks = []
self.deleted_tasks = [] # Trash bin
def delete_task(self, task_id: int):
"""Soft delete con undo window"""
task = self.find_task(task_id)
task.deleted_at = datetime.now()
task.undo_deadline = datetime.now() + timedelta(seconds=10)
self.deleted_tasks.append(task)
self.tasks.remove(task)
print(f"✅ Task deleted. [Undo] (10 seconds)")
# Schedule permanent delete
self.schedule_permanent_delete(task, delay=10)
def undo_delete(self, task_id: int):
"""Restore deleted task"""
task = self.find_deleted_task(task_id)
if datetime.now() > task.undo_deadline:
print("❌ Too late to undo")
return False
self.tasks.append(task)
self.deleted_tasks.remove(task)
print("↩️ Task restored")
return True
'''
print(f"{code}\n")
@staticmethod
def principle_4_consistency():
"""Consistency and Standards"""
print("4️⃣ CONSISTENCY AND STANDARDS")
print(" Usa patrones que los usuarios ya conocen\n")
print(" ✅ Standards:")
print(" • 'X' = close")
print(" • '🔍' = search")
print(" • Logo top-left lleva a home")
print(" • Cmd+S / Ctrl+S = save")
print(" • Red = danger, Green = success")
print("\n ❌ MAL: Inventar tus propios patterns")
print(" • '?' button para cerrar")
print(" • Search icon es una lupa azul rotada 45°")
print(" • Logo en bottom-right")
code = '''
# Design System: Mantén consistencia
class DesignSystem:
# Colors (use always the same)
COLORS = {
'primary': '#3B82F6', # Blue
'success': '#10B981', # Green
'danger': '#EF4444', # Red
'warning': '#F59E0B', # Orange
'info': '#6366F1' # Indigo
}
# Button sizes (don't mix)
BUTTON_SIZES = {
'small': 'px-3 py-1.5 text-sm',
'medium': 'px-4 py-2 text-base',
'large': 'px-6 py-3 text-lg'
}
# Spacing (use system, not random values)
SPACING = [0, 4, 8, 12, 16, 24, 32, 48, 64] # px
# Don't do this:
# margin: 13px ❌ (random)
# margin: 16px ✅ (from system)
'''
print(f"{code}\n")
@staticmethod
def principle_5_error_prevention():
"""Error Prevention"""
print("5️⃣ ERROR PREVENTION")
print(" Mejor prevenir errores que mostrar buenos mensajes de error\n")
print(" ✅ Strategies:")
print(" • Disable invalid actions")
print(" • Confirmation dialogs para acciones destructivas")
print(" • Input validation in real-time")
print(" • Defaults seguros")
code = '''
# Ejemplo: Prevenir errores en forms
class SmartForm:
def validate_email_realtime(self, email: str) -> dict:
"""Validar mientras el usuario escribe"""
if not email:
return {'valid': None, 'message': ''}
if '@' not in email:
return {
'valid': False,
'message': '⚠️ Email debe contener @'
}
if email.count('@') > 1:
return {
'valid': False,
'message': '⚠️ Solo un @ permitido'
}
domain = email.split('@')[1]
if '.' not in domain:
return {
'valid': False,
'message': '⚠️ Domain inválido (ej: example.com)'
}
return {
'valid': True,
'message': '✅ Email válido'
}
def prevent_destructive_action(self, action: str):
"""Require confirmation para acciones peligrosas"""
dangerous_actions = ['delete_all', 'reset_database', 'cancel_subscription']
if action in dangerous_actions:
# Require typing "DELETE" to confirm
confirmation = input(f"Type 'DELETE' to confirm {action}: ")
if confirmation != 'DELETE':
print("❌ Action cancelled")
return False
return True
'''
print(f"{code}\n")
@staticmethod
def print_all_principles():
"""Imprimir todos los principios"""
print("\n" + "🎨 " * 20)
print("UX PRINCIPLES FOR DEVELOPERS")
print("🎨 " * 20 + "\n")
UXPrinciples.principle_1_visibility()
print("-" * 80 + "\n")
UXPrinciples.principle_2_real_world()
print("-" * 80 + "\n")
UXPrinciples.principle_3_user_control()
print("-" * 80 + "\n")
UXPrinciples.principle_4_consistency()
print("-" * 80 + "\n")
UXPrinciples.principle_5_error_prevention()
UXPrinciples.print_all_principles()
Receta 7.7: Psicología del Usuario - Cognitive Load
¿Qué es? Cognitive load = cuánta “energía mental” requiere tu interfaz. Menos load = mejor UX.
3 tipos de Cognitive Load:
class CognitiveLoadPrinciples:
"""Reducir carga cognitiva del usuario"""
@staticmethod
def intrinsic_load():
"""Complexity inherente a la tarea"""
print("1️⃣ INTRINSIC LOAD (No puedes cambiar esto)")
print(" Es la complejidad inherente de la tarea")
print(" Ejemplo: Hacer taxes es inherentemente complejo\n")
print(" ✅ Qué puedes hacer:")
print(" • Divide tareas complejas en pasos simples")
print(" • Usa progressive disclosure (muestra info gradualmente)")
code = '''
# ❌ MAL: Todo de golpe
def create_account_form():
return {
'fields': [
'name', 'email', 'password', 'confirm_password',
'company', 'role', 'team_size', 'industry',
'phone', 'country', 'timezone', 'currency',
'billing_address', 'payment_method', 'promo_code'
]
}
# 15 campos = usuario overwhelmed
# ✅ BIEN: Multi-step wizard
def create_account_wizard():
return {
'step_1': ['name', 'email', 'password'], # Esencial
'step_2': ['company', 'role', 'team_size'], # Contexto
'step_3': ['payment_method', 'billing_address'] # Billing
}
# 3 steps × 3 campos = mucho más fácil
'''
print(f"{code}\n")
@staticmethod
def extraneous_load():
"""Complexity por mal diseño (PUEDES eliminar esto)"""
print("2️⃣ EXTRANEOUS LOAD (Elimina esto)")
print(" Complejidad causada por mal diseño\n")
print(" ❌ Causes:")
print(" • UI cluttered (demasiados elementos)")
print(" • Inconsistency")
print(" • Jerga técnica")
print(" • Pasos innecesarios")
code = '''
# ❌ MAL: Extraneous load
class BadCheckout:
def complete_purchase(self, cart):
# Step 1: Review cart
show_cart(cart)
# Step 2: Create account (WHY?? Already have cart!)
require_account_creation()
# Step 3: Verify email (FRICTION!)
send_verification_email()
wait_for_verification()
# Step 4: Enter shipping
get_shipping_info()
# Step 5: Enter billing (why separate from shipping?)
get_billing_info()
# Step 6: Review AGAIN
show_cart(cart) # We already did this!
# Step 7: Payment
process_payment()
# Total: 7 steps, 3 unnecessary
# ✅ BIEN: Minimal load
class GoodCheckout:
def complete_purchase(self, cart):
# Step 1: Shipping + Billing (combined)
info = get_shipping_and_billing()
# Step 2: Payment
process_payment(cart, info)
# Optional: Create account AFTER purchase (convert guest)
offer_account_creation()
# Total: 2 steps (or 1.5 if guest)
'''
print(f"{code}\n")
@staticmethod
def germane_load():
"""Complexity que ayuda al aprendizaje"""
print("3️⃣ GERMANE LOAD (Esto es bueno)")
print(" Complejidad que ayuda al usuario a aprender y mejorar\n")
print(" ✅ Examples:")
print(" • Tooltips que enseñan keyboard shortcuts")
print(" • Onboarding interactivo")
print(" • Context-aware help")
code = '''
# ✅ BIEN: Germane load que enseña
class SmartOnboarding:
def show_feature_tooltip(self, feature: str, first_use: bool):
"""Mostrar tooltip solo la primera vez"""
if first_use:
tooltips = {
'search': {
'message': 'Pro tip: Use Cmd+K to search anywhere',
'shortcut': '⌘K',
'helpful': True # User learns and gets faster
},
'bulk_edit': {
'message': 'Select multiple with Shift+Click',
'visual': show_animated_gif('shift_click.gif'),
'helpful': True
}
}
return tooltips.get(feature)
return None # Don't show tooltip again
def contextual_help(self, user_action: str):
"""Help basado en qué está tratando de hacer"""
if user_action == 'confused_about_pricing':
return {
'help': 'Our starter plan is best for teams under 10',
'comparison_table': show_plan_comparison(),
'video': '2-min explainer video'
}
if user_action == 'trying_advanced_feature':
return {
'help': 'This feature requires Pro plan',
'upgrade_cta': 'Upgrade to unlock',
'trial': 'Or start 14-day free trial'
}
'''
print(f"{code}\n")
@staticmethod
def print_all():
print("\n" + "🧠 " * 20)
print("COGNITIVE LOAD PRINCIPLES")
print("🧠 " * 20 + "\n")
CognitiveLoadPrinciples.intrinsic_load()
print("-" * 80 + "\n")
CognitiveLoadPrinciples.extraneous_load()
print("-" * 80 + "\n")
CognitiveLoadPrinciples.germane_load()
CognitiveLoadPrinciples.print_all()
Heurísticas para reducir cognitive load:
class CognitiveLoadHeuristics:
"""Técnicas prácticas para reducir load"""
@staticmethod
def heuristic_1_chunking():
"""Agrupa información relacionada"""
print("📦 CHUNKING - Agrupa info relacionada")
print(" Humanos recuerdan 7±2 items. Usa chunks.\n")
print(" ❌ MAL:")
print(" Phone: 5551234567")
print("\n ✅ BIEN:")
print(" Phone: (555) 123-4567")
print(" Credit card: 1234 5678 9012 3456")
print(" SSN: 123-45-6789\n")
code = '''
def format_credit_card(number: str) -> str:
"""Formato en chunks de 4"""
# ❌ MAL: 1234567890123456
# ✅ BIEN: 1234 5678 9012 3456
return ' '.join([number[i:i+4] for i in range(0, len(number), 4)])
'''
print(f"{code}\n")
@staticmethod
def heuristic_2_defaults():
"""Buenos defaults reducen decisiones"""
print("🎯 SMART DEFAULTS - Reduce decisiones")
print(" 80% de usuarios nunca cambian defaults\n")
code = '''
# ❌ MAL: Require todas las decisiones
class BadSignup:
def create_account(self):
timezone = ask_user_timezone() # They don't know!
date_format = ask_date_format() # They don't care!
currency = ask_currency() # Obvious from country!
theme = ask_theme() # Just pick one!
# ✅ BIEN: Smart defaults basados en contexto
class GoodSignup:
def create_account(self, ip_address: str):
# Detect desde IP
country = geolocate(ip_address)
defaults = {
'timezone': infer_timezone(country),
'currency': country_currency_map[country],
'date_format': country_date_format[country],
'theme': 'light', # Most popular
'language': detect_browser_language()
}
# User puede cambiar después si quiere
return create_with_defaults(defaults)
'''
print(f"{code}\n")
@staticmethod
def heuristic_3_progressive_disclosure():
"""Muestra solo lo necesario ahora"""
print("📖 PROGRESSIVE DISCLOSURE - Show gradually")
print(" No muestres todo de golpe\n")
code = '''
# ❌ MAL: Everything visible always
def settings_page():
return show_all_settings([
'account', 'billing', 'notifications', 'privacy',
'security', 'integrations', 'api_keys', 'webhooks',
'team', 'permissions', 'audit_log', 'data_export',
'danger_zone'
]) # 13 sections = overwhelming
# ✅ BIEN: Categorized + collapsed by default
def settings_page():
return {
'visible': ['account', 'notifications'], # Common
'collapsed': {
'Advanced': ['api_keys', 'webhooks'],
'Security': ['privacy', 'security', 'audit_log'],
'Billing': ['billing', 'data_export'],
'Danger Zone': ['danger_zone'] # Hidden by default
}
}
'''
print(f"{code}\n")
@staticmethod
def heuristic_4_recognition_vs_recall():
"""Recognition > Recall"""
print("👁️ RECOGNITION > RECALL")
print(" Más fácil reconocer que recordar\n")
print(" ❌ MAL (Recall): Dropdown vacío")
print(" Select country: [________]")
print("\n ✅ BIEN (Recognition): Dropdown con opciones")
print(" Select country: [🇺🇸 United States ▼]")
print(" 🇲🇽 Mexico")
print(" 🇨🇦 Canada\n")
code = '''
# ❌ MAL: Free text input (recall required)
def search_products():
query = input("Search: ") # User must remember exact name
# ✅ BIEN: Autocomplete (recognition)
def search_products_autocomplete():
query = input("Search: ")
if len(query) >= 2:
suggestions = get_suggestions(query)
show_dropdown(suggestions) # User recognizes what they want
'''
print(f"{code}\n")
@staticmethod
def print_all():
print("\n" + "💡 " * 20)
print("COGNITIVE LOAD REDUCTION HEURISTICS")
print("💡 " * 20 + "\n")
CognitiveLoadHeuristics.heuristic_1_chunking()
CognitiveLoadHeuristics.heuristic_2_defaults()
CognitiveLoadHeuristics.heuristic_3_progressive_disclosure()
CognitiveLoadHeuristics.heuristic_4_recognition_vs_recall()
CognitiveLoadHeuristics.print_all()
Receta 7.8: Prototipado Rápido con Figma
¿Qué es? Crear mockups interactivos ANTES de escribir código. 10 minutos en Figma te ahorran 10 horas de código.
Workflow: Idea → Prototype → Test → Code
class PrototypingWorkflow:
"""Proceso para validar UI antes de programar"""
@staticmethod
def step_1_wireframes():
"""Low-fidelity wireframes (papel o Figma)"""
print("STEP 1: WIREFRAMES (10-30 min)")
print("=" * 60)
print("Goal: Validar estructura y flujo\n")
print("Tools:")
print("• Papel + lápiz (más rápido)")
print("• Figma wireframe kit")
print("• Balsamiq\n")
print("Lo que DEBES incluir:")
print("✅ Layout básico (header, sidebar, content)")
print("✅ Navigation flow (qué pasa cuando click)")
print("✅ Key user actions (buttons, forms)")
print("\nLo que NO necesitas:")
print("❌ Colores")
print("❌ Fonts específicas")
print("❌ Imágenes reales")
print("❌ Copy final\n")
example = '''
Ejemplo wireframe (ASCII art):
+------------------------------------------+
| Logo Search... Login Signup|
+------------------------------------------+
| Dashboard | Projects | Settings |
+------------------------------------------+
| |
| [+ New Project] |
| |
| +------------------+ |
| | Project A | |
| | 23 tasks | |
| | [View] [Delete] | |
| +------------------+ |
| |
| +------------------+ |
| | Project B | |
| | 45 tasks | |
| | [View] [Delete] | |
| +------------------+ |
| |
+------------------------------------------+
'''
print(example)
@staticmethod
def step_2_mockups():
"""High-fidelity mockups con diseño visual"""
print("\nSTEP 2: HIGH-FIDELITY MOCKUPS (1-2 hours)")
print("=" * 60)
print("Goal: Diseño visual final antes de código\n")
print("Checklist:")
print("✅ Colors (usa design system)")
print("✅ Typography (2-3 fonts máximo)")
print("✅ Spacing (8px grid)")
print("✅ Icons (usa icon library)")
print("✅ Real copy (no lorem ipsum)")
print("✅ Responsive (desktop + mobile)\n")
print("Figma plugins útiles:")
print("• Iconify - 100k+ icons gratis")
print("• Unsplash - Stock photos")
print("• Content Reel - Real data")
print("• Stark - Accessibility checker\n")
@staticmethod
def step_3_prototype():
"""Interactive prototype para user testing"""
print("\nSTEP 3: INTERACTIVE PROTOTYPE (30 min)")
print("=" * 60)
print("Goal: Simular la app para testing\n")
print("Figma prototype features:")
print("• Click flows (button → new screen)")
print("• Hover states")
print("• Animations")
print("• Mobile gestures (swipe, scroll)\n")
print("Example flows to prototype:")
print("1. User signup flow")
print("2. Main user journey (ej: create → edit → delete)")
print("3. Error states (what happens when error?)")
print("4. Empty states (what shows when no data?)\n")
@staticmethod
def step_4_user_testing():
"""Test con 5 usuarios reales"""
print("\nSTEP 4: USER TESTING (2-3 hours total)")
print("=" * 60)
print("Goal: Encontrar problemas ANTES de programar\n")
print("Process:")
print("1. Recruit 5 users (friends, Twitter, UserTesting.com)")
print("2. Give them task: 'Sign up and create your first project'")
print("3. Watch them (Zoom screen share)")
print("4. Take notes (don't help them!)")
print("5. Ask questions after\n")
print("Red flags 🚩:")
print("• User clicks wrong thing")
print("• User says 'I don't understand'")
print("• User gives up")
print("• User asks 'What does this do?'")
print("• Takes >2 minutes for simple task\n")
print("If 3+ users struggle with same thing → FIX BEFORE CODING")
@staticmethod
def step_5_handoff_to_dev():
"""Developer handoff"""
print("\nSTEP 5: DEVELOPER HANDOFF")
print("=" * 60)
print("Goal: Hacer fácil implementar el diseño\n")
print("Figma Dev Mode features:")
print("✅ Inspect - Ver CSS automáticamente")
print("✅ Export assets (SVG, PNG)")
print("✅ Design tokens (colors, spacing)")
print("✅ Component specs\n")
print("Developer-friendly checklist:")
print("• Todas las pantallas nombradas claramente")
print("• Components organizados")
print("• Hover/active states documentados")
print("• Edge cases diseñados (loading, error, empty)")
print("• Responsive breakpoints especificados\n")
@staticmethod
def print_workflow():
print("\n" + "🎨 " * 20)
print("FIGMA PROTOTYPING WORKFLOW")
print("🎨 " * 20 + "\n")
PrototypingWorkflow.step_1_wireframes()
PrototypingWorkflow.step_2_mockups()
PrototypingWorkflow.step_3_prototype()
PrototypingWorkflow.step_4_user_testing()
PrototypingWorkflow.step_5_handoff_to_dev()
print("\n" + "=" * 60)
print("💡 ROI de Prototyping:")
print(" Time invested: 3-4 hours")
print(" Time saved: 10-20 hours de código descartado")
print(" User satisfaction: ⬆️ 40%")
print("=" * 60)
PrototypingWorkflow.print_workflow()
Receta 7.9: Accesibilidad (WCAG)
¿Qué es? Hacer tu producto usable por personas con discapacidades. No es opcional, es un requirement (y en algunos países, legal obligation).
WCAG Principles: POUR
class WCAGPrinciples:
"""Web Content Accessibility Guidelines"""
@staticmethod
def perceivable():
"""P - Perceivable: Usuarios deben poder percibir el contenido"""
print("1️⃣ PERCEIVABLE")
print(" La información debe ser presentable de forma que usuarios puedan percibirla\n")
print("✅ Text Alternatives (images, icons):")
code = '''
<!-- ❌ MAL: No alt text -->
<img src="logo.png">
<!-- ✅ BIEN: Alt text descriptivo -->
<img src="logo.png" alt="Acme Corp Logo">
<!-- ✅ BIEN: Decorative image (alt vacío) -->
<img src="decoration.png" alt="" role="presentation">
<!-- ❌ MAL: Icon buttons sin label -->
<button><i class="icon-trash"></i></button>
<!-- ✅ BIEN: Icon con aria-label -->
<button aria-label="Delete item">
<i class="icon-trash" aria-hidden="true"></i>
</button>
'''
print(code)
print("\n✅ Color Contrast (mínimo 4.5:1):")
code2 = '''
# Calcular contrast ratio
def calculate_contrast_ratio(color1_rgb, color2_rgb):
def luminance(rgb):
# Convert to linear RGB
def adjust(c):
c = c / 255.0
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
r, g, b = [adjust(c) for c in rgb]
return 0.2126 * r + 0.7152 * g + 0.0722 * b
l1 = luminance(color1_rgb)
l2 = luminance(color2_rgb)
lighter = max(l1, l2)
darker = min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
# Example
white = (255, 255, 255)
light_gray = (170, 170, 170)
ratio = calculate_contrast_ratio(white, light_gray)
print(f"Contrast: {ratio:.2f}:1")
if ratio >= 4.5:
print("✅ WCAG AA compliant")
elif ratio >= 3.0:
print("⚠️ WCAG AA compliant for large text only")
else:
print("❌ Does not meet WCAG AA")
'''
print(code2)
print()
@staticmethod
def operable():
"""O - Operable: Usuarios deben poder operar la interfaz"""
print("\n2️⃣ OPERABLE")
print(" La interfaz debe ser operable con teclado y otros input methods\n")
print("✅ Keyboard Navigation:")
code = '''
<!-- ❌ MAL: onClick en div (no keyboard accessible) -->
<div onclick="doSomething()">Click me</div>
<!-- ✅ BIEN: Usar button (inherently keyboard accessible) -->
<button onclick="doSomething()">Click me</button>
<!-- ✅ BIEN: Custom focus styles -->
<style>
button:focus {
outline: 3px solid #3B82F6; /* Visible focus indicator */
outline-offset: 2px;
}
/* ❌ NUNCA hagas esto: */
*:focus {
outline: none; /* Elimina focus = bad accessibility */
}
</style>
<!-- ✅ BIEN: Skip to content link -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
'''
print(code)
print("\n✅ Focus Management:")
code2 = '''
# Ejemplo: Focus trap en modal
class AccessibleModal:
def __init__(self):
self.focusable_elements = []
self.first_focusable = None
self.last_focusable = None
def open_modal(self):
# 1. Store element that opened modal
self.trigger_element = document.activeElement
# 2. Get all focusable elements in modal
self.focusable_elements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
self.first_focusable = self.focusable_elements[0]
self.last_focusable = self.focusable_elements[-1]
# 3. Focus first element
self.first_focusable.focus()
# 4. Trap focus inside modal
modal.addEventListener('keydown', self.handle_tab)
def handle_tab(self, event):
"""Keep focus inside modal"""
if event.key == 'Tab':
if event.shiftKey: # Shift + Tab
if document.activeElement == self.first_focusable:
event.preventDefault()
self.last_focusable.focus()
else: # Tab
if document.activeElement == self.last_focusable:
event.preventDefault()
self.first_focusable.focus()
def close_modal(self):
# Return focus to trigger element
self.trigger_element.focus()
'''
print(code2)
print()
@staticmethod
def understandable():
"""U - Understandable: Usuarios deben poder entender la información"""
print("\n3️⃣ UNDERSTANDABLE")
print(" El contenido y la operación deben ser comprensibles\n")
print("✅ Error Prevention & Recovery:")
code = '''
<!-- ❌ MAL: Error genérico -->
<span class="error">Invalid input</span>
<!-- ✅ BIEN: Error descriptivo con solution -->
<div role="alert" class="error">
<p id="email-error">
Email must include @ symbol.
Example: user@example.com
</p>
</div>
<input
type="email"
aria-describedby="email-error"
aria-invalid="true"
>
<!-- ✅ BIEN: Form validation con clear instructions -->
<label for="password">
Password
<span class="hint">Must be 8+ characters with 1 number</span>
</label>
<input
type="password"
id="password"
aria-describedby="password-hint"
minlength="8"
required
>
<span id="password-hint" class="sr-only">
Password requirements: minimum 8 characters including at least 1 number
</span>
'''
print(code)
print("\n✅ Consistent Navigation:")
code2 = '''
# ✅ BIEN: Consistent heading structure
# <h1>Page Title</h1> # Only one h1
# <h2>Section 1</h2>
# <h3>Subsection 1.1</h3>
# <h3>Subsection 1.2</h3>
# <h2>Section 2</h2>
# <h3>Subsection 2.1</h3>
# ❌ MAL: Skipping heading levels
# <h1>Page Title</h1>
# <h4>Subsection</h4> # Missing h2, h3
'''
print(code2)
print()
@staticmethod
def robust():
"""R - Robust: Compatible con assistive technologies"""
print("\n4️⃣ ROBUST")
print(" El contenido debe ser compatible con screen readers y otras AT\n")
print("✅ Semantic HTML:")
code = '''
<!-- ❌ MAL: Div soup -->
<div class="navigation">
<div class="nav-item">Home</div>
<div class="nav-item">About</div>
</div>
<div class="main-content">
<div class="article">
<div class="title">Article Title</div>
<div class="text">Article content...</div>
</div>
</div>
<!-- ✅ BIEN: Semantic HTML -->
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<main>
<article>
<h2>Article Title</h2>
<p>Article content...</p>
</article>
</main>
'''
print(code)
print("\n✅ ARIA roles & labels:")
code2 = '''
<!-- Custom components need ARIA -->
<!-- ✅ BIEN: Custom dropdown -->
<div class="dropdown">
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="dropdown-menu"
id="dropdown-button"
>
Options
</button>
<ul
role="menu"
id="dropdown-menu"
aria-labelledby="dropdown-button"
hidden
>
<li role="menuitem">
<a href="/edit">Edit</a>
</li>
<li role="menuitem">
<a href="/delete">Delete</a>
</li>
</ul>
</div>
<!-- ✅ BIEN: Loading state -->
<div
role="status"
aria-live="polite"
aria-busy="true"
>
Loading data...
</div>
<!-- Screen reader announces updates -->
<div
role="alert"
aria-live="assertive"
>
Form submitted successfully!
</div>
'''
print(code2)
print()
@staticmethod
def print_all():
print("\n" + "♿ " * 20)
print("WCAG ACCESSIBILITY PRINCIPLES (POUR)")
print("♿ " * 20)
WCAGPrinciples.perceivable()
WCAGPrinciples.operable()
WCAGPrinciples.understandable()
WCAGPrinciples.robust()
print("\n" + "=" * 70)
print("🧰 Testing Tools:")
print(" • axe DevTools (Chrome extension)")
print(" • WAVE (Web Accessibility Evaluation Tool)")
print(" • Lighthouse (Chrome DevTools)")
print(" • Screen reader (NVDA gratis, JAWS, VoiceOver)")
print("=" * 70)
WCAGPrinciples.print_all()
Accessibility checklist:
class AccessibilityChecklist:
"""Quick checklist para cada feature"""
@staticmethod
def print_checklist():
checklist = """
♿ ACCESSIBILITY CHECKLIST
========================
Before shipping ANY feature, verify:
☐ 1. KEYBOARD NAVIGATION
• Can you use Tab to navigate?
• Can you activate with Enter/Space?
• Is focus visible?
• No keyboard traps?
☐ 2. SCREEN READER
• Images have alt text?
• Buttons have labels?
• Form inputs have labels?
• Heading hierarchy correct?
☐ 3. COLOR & CONTRAST
• Text contrast ≥ 4.5:1?
• Don't rely only on color to convey info?
• Visible in high contrast mode?
☐ 4. FORMS
• Labels associated with inputs?
• Error messages descriptive?
• Required fields marked?
☐ 5. DYNAMIC CONTENT
• ARIA live regions for updates?
• Focus management in modals?
• Loading states announced?
☐ 6. MOBILE
• Touch targets ≥ 44x44px?
• Works with screen reader?
• Zoom doesn't break layout?
🧪 Test with:
• Keyboard only (unplug mouse)
• Screen reader (NVDA/VoiceOver)
• Browser zoom at 200%
• High contrast mode
"""
print(checklist)
AccessibilityChecklist.print_checklist()