Ver contenido
Guía de Implementación de Redimensionar Texto: Patrones Completos para Tipografía Escalable
Introducción
Crear tipografía escalable que funcione al 200% de zoom requiere atención cuidadosa a las unidades CSS, dimensionamiento de contenedores y flexibilidad del diseño. Esta guía cubre patrones completos de implementación para texto amigable con el redimensionamiento que cumple con los requisitos de WCAG 1.4.4 mientras mantiene calidad visual a cualquier tamaño.
Más allá del cumplimiento, la tipografía escalable bien implementada mejora la experiencia de usuario en todos los dispositivos y acomoda preferencias diversas de los usuarios.
Sistema de Unidades CSS para Texto Escalable
Entendiendo Unidades Relativas
/*
rem = relativo al tamaño de fuente del elemento raíz (html)
em = relativo al tamaño de fuente del elemento padre
% = relativo al elemento padre
vw/vh = relativo al ancho/alto del viewport
Tamaño de fuente predeterminado del navegador: 16px
1rem = 16px (por defecto)
1em = tamaño de fuente del padre
*/
/* DIMENSIONAMIENTO RAÍZ: Establecer base en html para cálculos predecibles de rem */
html {
font-size: 100%; /* Respeta configuraciones del navegador del usuario */
/* Evitar: font-size: 16px; - anula preferencias del usuario */
}
/* VALORES PREDETERMINADOS DEL BODY */
body {
font-size: 1rem; /* 16px por defecto */
line-height: 1.5; /* Sin unidades para mejor escalado */
letter-spacing: 0.01em; /* Escala con el texto */
}
Escala Tipográfica con rem
/* Escala tipográfica usando rem para dimensionamiento consistente */
:root {
/* Escala modular (ratio 1.25 - Tercera Mayor) */
--text-xs: 0.64rem; /* 10.24px */
--text-sm: 0.8rem; /* 12.8px */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
--text-xl: 1.563rem; /* 25px */
--text-2xl: 1.953rem; /* 31.25px */
--text-3xl: 2.441rem; /* 39px */
--text-4xl: 3.052rem; /* 48.83px */
}
/* Aplicar a elementos */
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
h5 { font-size: var(--text-lg); }
h6 { font-size: var(--text-base); }
p, li, td { font-size: var(--text-base); }
small, .caption { font-size: var(--text-sm); }
Tipografía Fluida con clamp()
/* Tipografía fluida que escala suavemente */
:root {
/* clamp(mínimo, preferido, máximo) */
--fluid-sm: clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem);
--fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--fluid-lg: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
--fluid-xl: clamp(1.5rem, 1.25rem + 1.25vw, 2rem);
--fluid-2xl: clamp(2rem, 1.5rem + 2.5vw, 3rem);
--fluid-3xl: clamp(2.5rem, 2rem + 2.5vw, 4rem);
}
/* Uso */
h1 {
font-size: var(--fluid-3xl);
/* Escala entre 2.5rem y 4rem basado en viewport */
/* Siempre respeta el zoom del 200% */
}
body {
font-size: var(--fluid-base);
}
Patrones de Contenedores
Contenedores de Altura Flexible
/* INCORRECTO: La altura fija corta texto en tamaños más grandes */
.card-header {
height: 60px;
overflow: hidden;
}
/* CORRECTO: Altura mínima con crecimiento */
.card-header {
min-height: 60px;
padding: 1em;
/* El contenido puede expandirse */
}
/* CORRECTO: Altura automática con padding */
.card-content {
padding: 1.5em;
/* Altura determinada por contenido */
}
Patrones de Ancho Flexible
/* Los contenedores de texto deben ser flexibles */
.text-container {
width: 100%;
max-width: 65ch; /* Longitud óptima de lectura */
padding: 1em;
}
/* Barra lateral que no se rompe con zoom */
.sidebar {
flex: 0 0 auto;
width: clamp(200px, 25%, 300px);
min-width: min-content; /* Nunca más pequeño que el contenido */
}
/* Área de contenido principal */
.main-content {
flex: 1 1 auto;
min-width: 0; /* Permite encogimiento flex */
}
Layout Grid para Zoom
/* Grid que se adapta al tamaño del contenido */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: 1.5rem;
}
/* Este grid:
- Las tarjetas tienen mínimo 280px de ancho
- O 100% si el viewport es menor a 280px
- Se reorganiza automáticamente con zoom
*/
Implementación en React
Componente de Tipografía Escalable
import React from 'react';
interface TextProps {
as?: 'p' | 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'small';
size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
children: React.ReactNode;
className?: string;
}
const sizeClasses = {
xs: 'text-xs', // 0.64rem
sm: 'text-sm', // 0.8rem
base: 'text-base', // 1rem
lg: 'text-lg', // 1.25rem
xl: 'text-xl', // 1.563rem
'2xl': 'text-2xl', // 1.953rem
'3xl': 'text-3xl', // 2.441rem
'4xl': 'text-4xl', // 3.052rem
};
export function Text({
as: Component = 'p',
size = 'base',
children,
className = ''
}: TextProps) {
return (
<Component className={`${sizeClasses[size]} ${className}`}>
{children}
</Component>
);
}
// Componente de contenedor flexible
interface ContainerProps {
children: React.ReactNode;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'prose';
className?: string;
}
const maxWidthClasses = {
sm: 'max-w-sm', // 24rem
md: 'max-w-md', // 28rem
lg: 'max-w-lg', // 32rem
xl: 'max-w-xl', // 36rem
prose: 'max-w-prose' // 65ch
};
export function Container({
children,
maxWidth = 'prose',
className = ''
}: ContainerProps) {
return (
<div className={`w-full ${maxWidthClasses[maxWidth]} px-4 ${className}`}>
{children}
</div>
);
}
Implementación en Vue 3
Sistema de Tipografía
<!-- composables/useTypography.ts -->
<script setup lang="ts">
import { computed } from 'vue'
type TextSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
const sizeMap: Record<TextSize, string> = {
xs: '0.64rem',
sm: '0.8rem',
base: '1rem',
lg: '1.25rem',
xl: '1.563rem',
'2xl': '1.953rem',
'3xl': '2.441rem',
'4xl': '3.052rem'
}
export function useTypography(size: TextSize = 'base') {
const fontSize = computed(() => sizeMap[size])
const style = computed(() => ({
fontSize: fontSize.value,
lineHeight: 1.5
}))
return { fontSize, style }
}
</script>
<!-- components/ScalableText.vue -->
<script setup lang="ts">
import { computed } from 'vue'
type TextSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'
interface Props {
as?: string
size?: TextSize
}
const props = withDefaults(defineProps<Props>(), {
as: 'p',
size: 'base'
})
const sizeClasses: Record<TextSize, string> = {
xs: 'text-xs',
sm: 'text-sm',
base: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl'
}
const textClass = computed(() => sizeClasses[props.size])
</script>
<template>
<component :is="as" :class="textClass">
<slot />
</component>
</template>
<style scoped>
.text-xs { font-size: 0.64rem; }
.text-sm { font-size: 0.8rem; }
.text-base { font-size: 1rem; }
.text-lg { font-size: 1.25rem; }
.text-xl { font-size: 1.563rem; }
.text-2xl { font-size: 1.953rem; }
.text-3xl { font-size: 2.441rem; }
.text-4xl { font-size: 3.052rem; }
</style>
Configuración de Tailwind CSS
Escala Tipográfica Personalizada
// tailwind.config.js
module.exports = {
theme: {
fontSize: {
'xs': ['0.64rem', { lineHeight: '1.5' }],
'sm': ['0.8rem', { lineHeight: '1.5' }],
'base': ['1rem', { lineHeight: '1.5' }],
'lg': ['1.25rem', { lineHeight: '1.4' }],
'xl': ['1.563rem', { lineHeight: '1.3' }],
'2xl': ['1.953rem', { lineHeight: '1.2' }],
'3xl': ['2.441rem', { lineHeight: '1.2' }],
'4xl': ['3.052rem', { lineHeight: '1.1' }],
},
extend: {
// Utilidades de tipografía fluida
fontSize: {
'fluid-sm': 'clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem)',
'fluid-base': 'clamp(1rem, 0.9rem + 0.5vw, 1.125rem)',
'fluid-lg': 'clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem)',
'fluid-xl': 'clamp(1.5rem, 1.25rem + 1.25vw, 2rem)',
'fluid-2xl': 'clamp(2rem, 1.5rem + 2.5vw, 3rem)',
'fluid-3xl': 'clamp(2.5rem, 2rem + 2.5vw, 4rem)',
}
}
}
}
Pruebas Automatizadas
Tests de Playwright para Redimensionamiento de Texto
import { test, expect } from '@playwright/test';
test.describe('Accesibilidad de Redimensionamiento de Texto', () => {
test('el contenido permanece visible al 200% de zoom', async ({ page }) => {
await page.goto('/');
// Obtener viewport inicial
const viewportSize = page.viewportSize();
// Simular 200% de zoom reduciendo viewport a la mitad
await page.setViewportSize({
width: viewportSize.width / 2,
height: viewportSize.height / 2
});
// Verificar que el contenido clave sigue visible
const heading = page.locator('h1').first();
await expect(heading).toBeVisible();
// Verificar que no se necesita barra de desplazamiento horizontal para contenido principal
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
// Algo de desplazamiento horizontal puede ser aceptable, pero excesivo no
const mainContent = page.locator('main');
const mainBox = await mainContent.boundingBox();
expect(mainBox.width).toBeLessThanOrEqual(viewportSize.width / 2 + 20);
});
test('el texto no está cortado en contenedores', async ({ page }) => {
await page.goto('/');
// Zoom al 200%
await page.setViewportSize({
width: 640,
height: 480
});
// Encontrar todos los contenedores de texto
const textContainers = page.locator('p, h1, h2, h3, h4, h5, h6, li, td, th');
const count = await textContainers.count();
for (let i = 0; i < Math.min(count, 20); i++) {
const container = textContainers.nth(i);
const isVisible = await container.isVisible();
if (isVisible) {
// Verificar que overflow no está oculto con recorte de contenido
const hasOverflowHidden = await container.evaluate(el => {
const style = window.getComputedStyle(el);
const parent = el.parentElement;
const parentStyle = parent ? window.getComputedStyle(parent) : null;
// Verificar si el contenido está recortado
return (style.overflow === 'hidden' || parentStyle?.overflow === 'hidden') &&
(el.scrollHeight > el.clientHeight);
});
expect(hasOverflowHidden).toBe(false);
}
}
});
test('los tamaños de fuente usan unidades relativas', async ({ page }) => {
await page.goto('/');
// Verificar que body y texto principal usan unidades relativas
const textElements = page.locator('body, p, h1, h2, h3, h4, h5, h6');
const count = await textElements.count();
for (let i = 0; i < count; i++) {
const element = textElements.nth(i);
// El estilo en línea no debe tener font-size en px fijos
const inlineStyle = await element.getAttribute('style');
if (inlineStyle) {
expect(inlineStyle).not.toMatch(/font-size:\s*\d+px/);
}
}
});
test('los elementos interactivos permanecen usables con zoom', async ({ page }) => {
await page.goto('/');
// Zoom al 200%
await page.setViewportSize({ width: 640, height: 480 });
// Verificar que los botones siguen visibles y clicables
const buttons = page.locator('button, [role="button"], a');
const count = await buttons.count();
for (let i = 0; i < Math.min(count, 10); i++) {
const button = buttons.nth(i);
const isVisible = await button.isVisible();
if (isVisible) {
const box = await button.boundingBox();
expect(box).not.toBeNull();
expect(box.width).toBeGreaterThan(0);
expect(box.height).toBeGreaterThan(0);
}
}
});
});
Checklist de Resumen
- [ ] Todos los tamaños de fuente usan unidades rem o em
- [ ] El tamaño de fuente raíz respeta preferencias del usuario (usar % o nada)
- [ ] Las alturas de línea son sin unidades para escalado apropiado
- [ ] Los contenedores usan min-height en lugar de altura fija
- [ ] Los layouts usan anchos flexibles (%, flex, grid)
- [ ] No hay overflow:hidden en contenedores de texto
- [ ] El contenido permanece visible al 200% de zoom
- [ ] No se requiere desplazamiento horizontal para contenido principal
- [ ] Los elementos interactivos permanecen accesibles con zoom
- [ ] Probado con zoom del navegador y zoom solo de texto