Ver contenido
Introducción
WCAG 1.4.5 (Imágenes de Texto) requiere que si las tecnologías utilizadas pueden lograr la presentación visual, se use texto para transmitir información en lugar de imágenes de texto. Esto significa usar texto HTML real con estilos CSS en lugar de incrustar texto dentro de imágenes.
Aunque las imágenes de texto pueden parecer una forma fácil de lograr tipografía específica, crean barreras significativas de accesibilidad y problemas de usabilidad para muchos usuarios.
Qué Requiere WCAG
1.4.5 Imágenes de Texto (Nivel AA)
Si las tecnologías utilizadas pueden lograr la presentación visual, se usa texto para transmitir información en lugar de imágenes de texto.
Excepciones:
- Personalizable: La imagen puede personalizarse visualmente según los requisitos del usuario
- Esencial: Una presentación particular del texto es esencial (como logos o marca)
Esto significa:
- Usar CSS para estilizar texto en lugar de crear texto como imágenes
- Se permiten logos que contienen texto
- El texto esencial para transmitir información (como nombres de marca) puede ser imágenes
Quién Se Beneficia
| Tipo de Usuario | Beneficio |
|---|---|
| Usuarios con baja visión | Pueden agrandar texto sin pixelación |
| Usuarios de lectores de pantalla | El texto se lee naturalmente, no como “imagen” |
| Usuarios que necesitan alto contraste | Pueden aplicar hojas de estilo personalizadas |
| Usuarios con dislexia | Pueden cambiar fuentes para legibilidad |
| Motores de búsqueda | Pueden indexar y entender el contenido |
| Todos los usuarios | El texto carga más rápido, escala mejor |
Problemas con Imágenes de Texto
1. No Se Pueden Redimensionar
<!-- MAL: El texto de imagen se pixela cuando se agranda -->
<img src="heading-text.png" alt="Bienvenido a Nuestro Sitio">
<!-- BIEN: El texto real escala suavemente -->
<h1 class="custom-heading">Bienvenido a Nuestro Sitio</h1>
2. No Se Pueden Personalizar
Los usuarios con discapacidades visuales a menudo usan configuraciones del navegador o extensiones para:
- Cambiar tamaños de fuente
- Aplicar temas de alto contraste
- Usar fuentes específicas (como OpenDyslexic)
- Ajustar espaciado de línea
Las imágenes de texto no pueden responder a estas preferencias del usuario.
3. Lentas para Cargar
<!-- Imagen de texto: 50-200KB por imagen -->
<img src="large-decorative-heading.png" alt="Oferta Especial">
<!-- Texto real con CSS: <1KB -->
<h2 class="decorative-heading">Oferta Especial</h2>
4. Problemas de Accesibilidad
- Los lectores de pantalla anuncian “imagen” en lugar de leer naturalmente
- No se puede seleccionar ni copiar
- Las herramientas de traducción no pueden procesar el texto
- No se puede buscar con búsqueda en página (Ctrl+F)
Escenarios Comunes
Usos Aceptables de Imágenes de Texto
Logos y Marca
<!-- Aceptable: El logo contiene presentación esencial de marca -->
<img src="company-logo.svg" alt="Acme Corporation">
Diagramas con Texto Esencial
<!-- Aceptable: El texto es parte del significado esencial del diagrama -->
<figure>
<img src="flowchart.png" alt="Diagrama de flujo mostrando pasos...">
<figcaption>Figura 1: Flujo de trabajo del proceso de aprobación</figcaption>
</figure>
Usos Inaceptables
Titulares y Texto de Cuerpo
<!-- MAL: Usando imagen para titular -->
<img src="headline.png" alt="Nuestros Servicios">
<!-- BIEN: Texto real con estilo personalizado -->
<h2 class="services-heading">Nuestros Servicios</h2>
Botones y Navegación
<!-- MAL: Botón de imagen -->
<button><img src="submit-button.png" alt="Enviar"></button>
<!-- BIEN: Botón de texto estilizado -->
<button class="submit-btn">Enviar</button>
Secciones de Llamada a la Acción
<!-- MAL: Imagen de banner con texto -->
<img src="sale-banner.png" alt="50% Descuento en Todo - Compra Ahora">
<!-- BIEN: HTML con estilo CSS -->
<section class="sale-banner">
<h2>50% Descuento en Todo</h2>
<a href="/tienda" class="cta-button">Compra Ahora</a>
</section>
Alternativas CSS a Imágenes de Texto
Fuentes Personalizadas
/* Usar web fonts para tipografía personalizada */
@font-face {
font-family: 'CustomHeading';
src: url('/fonts/custom-heading.woff2') format('woff2');
font-display: swap;
}
.decorative-heading {
font-family: 'CustomHeading', serif;
font-size: 3rem;
}
Efectos de Texto
/* Texto con gradiente */
.gradient-text {
background: linear-gradient(45deg, #e66465, #9198e5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Sombra de texto para profundidad */
.shadow-text {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
/* Texto con contorno */
.outline-text {
-webkit-text-stroke: 2px #000;
color: transparent;
}
Tipografía Responsiva
/* Texto que escala con el viewport */
.hero-heading {
font-size: clamp(2rem, 5vw, 5rem);
font-weight: 700;
letter-spacing: -0.02em;
}
Probando Imágenes de Texto
Prueba Manual Rápida
- Haz clic derecho en el texto de la página
- Si aparece “Guardar imagen” o “Copiar imagen”, es probablemente una imagen de texto
- Intenta seleccionar el texto - si no puedes resaltarlo, puede ser una imagen
- Usa “Inspeccionar Elemento” del navegador para verificar si es
<img>o texto
Qué Verificar
- [ ] Los titulares usan elementos
<h1>-<h6>reales, no imágenes - [ ] El contenido del cuerpo es texto real, no imágenes
- [ ] Los botones contienen texto, no etiquetas de imagen
- [ ] Los efectos de texto decorativo usan CSS, no imágenes
- [ ] Los logos son las únicas imágenes que contienen texto (y tienen alt text apropiado)
Herramientas para Pruebas
- WAVE: Resalta imágenes y su texto alternativo
- axe DevTools: Reporta problemas de imágenes de texto
- DevTools del navegador: Inspeccionar elementos para verificar texto vs imágenes
- Lector de pantalla: Escuchar lectura natural de texto vs anuncios de “imagen”
Resumen de Mejores Prácticas
| Hacer | No Hacer |
|---|---|
| Usar CSS para estilizar texto | Crear imágenes con texto |
| Usar web fonts para tipografía personalizada | Capturar texto para fuentes únicas |
| Estilizar botones con CSS | Usar botones de imagen |
| Usar iconos SVG con texto separado | Incrustar texto en iconos |
| Mantener logos como únicas imágenes con texto | Usar imágenes para titulares |
Implementación
Implementación de Web Fonts
Integración de Google Fonts
<!-- En <head> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap" rel="stylesheet">
/* Uso */
.encabezado-decorativo {
font-family: 'Playfair Display', Georgia, serif;
font-weight: 700;
font-size: 3rem;
}
Fuentes Auto-Alojadas
/* Declaración font-face para fuentes personalizadas */
@font-face {
font-family: 'FuenteMarca';
src: url('/fonts/fuentemarca.woff2') format('woff2'),
url('/fonts/fuentemarca.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap; /* Previene texto invisible durante carga */
}
@font-face {
font-family: 'FuenteMarca';
src: url('/fonts/fuentemarca-bold.woff2') format('woff2'),
url('/fonts/fuentemarca-bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Fuente variable (archivo único, múltiples pesos) */
@font-face {
font-family: 'FuenteMarcaVariable';
src: url('/fonts/fuentemarca-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
Efectos de Texto CSS
Texto con Gradiente
/* Relleno gradiente en texto */
.texto-gradiente {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent; /* Fallback */
}
/* Gradiente animado */
.texto-gradiente-animado {
background: linear-gradient(
90deg,
#ff6b6b,
#feca57,
#48dbfb,
#ff9ff3,
#ff6b6b
);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: desplazar-gradiente 8s ease infinite;
}
@keyframes desplazar-gradiente {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
Sombras de Texto
/* Sombra sutil para profundidad */
.sombra-sutil {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Sombra pronunciada */
.sombra-fuerte {
text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
}
/* Texto brillante */
.texto-brillo {
text-shadow:
0 0 10px rgba(255, 255, 255, 0.8),
0 0 20px rgba(255, 255, 255, 0.6),
0 0 30px rgba(255, 255, 255, 0.4);
}
/* Efecto neón */
.texto-neon {
color: #fff;
text-shadow:
0 0 5px #fff,
0 0 10px #fff,
0 0 20px #0ff,
0 0 30px #0ff,
0 0 40px #0ff;
}
/* Efecto 3D retro */
.retro-3d {
color: #f5f5f5;
text-shadow:
1px 1px 0 #c4c4c4,
2px 2px 0 #b4b4b4,
3px 3px 0 #a4a4a4,
4px 4px 0 #949494;
}
Trazo/Contorno de Texto
/* Texto con contorno */
.texto-contorno {
-webkit-text-stroke: 2px #333;
color: transparent;
}
/* Relleno con trazo */
.trazo-relleno {
-webkit-text-stroke: 1px #000;
color: #fff;
}
/* Efecto doble contorno */
.doble-contorno {
color: #fff;
-webkit-text-stroke: 3px #000;
paint-order: stroke fill;
}
Espaciado de Letras y Transformaciones
/* Mayúsculas espaciadas (ideal para encabezados) */
.mayusculas-espaciadas {
text-transform: uppercase;
letter-spacing: 0.2em;
font-weight: 600;
}
/* Versalitas */
.versalitas {
font-variant: small-caps;
letter-spacing: 0.05em;
}
/* Espaciado condensado */
.espaciado-ajustado {
letter-spacing: -0.02em;
}
Componentes de Tipografía Decorativa
Componente React
import React from 'react';
interface DecorativeTextProps {
children: React.ReactNode;
variant: 'gradient' | 'shadow' | 'outline' | 'glow';
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'span' | 'p';
className?: string;
}
const variantStyles = {
gradient: `
bg-gradient-to-r from-purple-600 to-pink-600
bg-clip-text text-transparent
`,
shadow: `
text-gray-900
[text-shadow:_3px_3px_0_rgb(0_0_0_/_20%)]
`,
outline: `
text-transparent
[-webkit-text-stroke:_2px_currentColor]
text-gray-900
`,
glow: `
text-white
[text-shadow:_0_0_10px_rgba(255,255,255,0.8),_0_0_20px_rgba(255,255,255,0.6)]
`
};
export function DecorativeText({
children,
variant,
as: Component = 'span',
className = ''
}: DecorativeTextProps) {
return (
<Component className={`${variantStyles[variant]} ${className}`}>
{children}
</Component>
);
}
// Uso
<DecorativeText variant="gradient" as="h1" className="text-5xl font-bold">
Encabezado Hermoso
</DecorativeText>
Componente Vue 3
<script setup lang="ts">
import { computed } from 'vue'
type TextVariant = 'gradient' | 'shadow' | 'outline' | 'glow'
interface Props {
variant: TextVariant
as?: string
}
const props = withDefaults(defineProps<Props>(), {
as: 'span'
})
const variantClasses = computed(() => {
const variants: Record<TextVariant, string> = {
gradient: 'decorative-gradient',
shadow: 'decorative-shadow',
outline: 'decorative-outline',
glow: 'decorative-glow'
}
return variants[props.variant]
})
</script>
<template>
<component :is="as" :class="variantClasses">
<slot />
</component>
</template>
<style scoped>
.decorative-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.decorative-shadow {
text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
}
.decorative-outline {
-webkit-text-stroke: 2px currentColor;
color: transparent;
}
.decorative-glow {
color: #fff;
text-shadow:
0 0 10px rgba(255, 255, 255, 0.8),
0 0 20px rgba(255, 255, 255, 0.6);
}
</style>
Convirtiendo Imágenes de Texto a CSS
Antes: Banner de Imagen
<!-- ANTIGUO: Imagen con texto incrustado -->
<img src="banner-oferta.png" alt="Oferta de Verano - Hasta 50% de Descuento - Compra Ahora">
Después: Banner CSS
<section class="banner-oferta">
<div class="banner-contenido">
<p class="banner-subtitulo">Tiempo Limitado</p>
<h2 class="banner-titulo">Oferta de Verano</h2>
<p class="banner-descuento">Hasta <span class="resaltado">50% de Descuento</span></p>
<a href="/ofertas" class="banner-cta">Compra Ahora</a>
</div>
</section>
.banner-oferta {
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%);
padding: 3rem;
text-align: center;
border-radius: 1rem;
}
.banner-subtitulo {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 0.5rem;
}
.banner-titulo {
font-family: 'Playfair Display', serif;
font-size: clamp(2rem, 5vw, 4rem);
color: #fff;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.banner-descuento {
font-size: 1.5rem;
color: #fff;
}
.resaltado {
font-weight: 700;
font-size: 2rem;
}
.banner-cta {
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
background: #fff;
color: #ff6b6b;
font-weight: 600;
border-radius: 2rem;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.banner-cta:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
Texto SVG para Efectos Complejos
<!-- Texto SVG con efectos no posibles en CSS -->
<svg viewBox="0 0 400 100" class="encabezado-svg" aria-labelledby="titulo-svg">
<title id="titulo-svg">Tipografía Creativa</title>
<defs>
<linearGradient id="gradiente-texto" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
<filter id="sombra">
<feDropShadow dx="2" dy="2" stdDeviation="2" flood-opacity="0.3"/>
</filter>
</defs>
<text
x="50%"
y="50%"
dominant-baseline="middle"
text-anchor="middle"
fill="url(#gradiente-texto)"
filter="url(#sombra)"
font-family="Georgia, serif"
font-size="48"
font-weight="bold"
>
Tipografía Creativa
</text>
</svg>
.encabezado-svg {
width: 100%;
max-width: 400px;
height: auto;
}
/* El texto SVG hereda la fuente de CSS si no se especifica */
.encabezado-svg text {
font-family: inherit;
}
Pruebas Automatizadas
Pruebas Playwright para Imágenes de Texto
import { test, expect } from '@playwright/test';
test.describe('Cumplimiento de Imágenes de Texto', () => {
test('ninguna imagen contiene texto excepto logos', async ({ page }) => {
await page.goto('/');
// Obtener todas las imágenes
const images = page.locator('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const img = images.nth(i);
const alt = await img.getAttribute('alt') || '';
const src = await img.getAttribute('src') || '';
const className = await img.getAttribute('class') || '';
// Omitir imágenes de logo (aceptables)
const esLogo = className.includes('logo') ||
src.includes('logo') ||
alt.toLowerCase().includes('logo');
if (!esLogo) {
// Verificar si el texto alt sugiere que es contenido de texto
const tieneContenidoTexto = /^[A-Z]/.test(alt) && // Comienza con mayúscula
alt.length > 20 && // Suficientemente largo para ser una oración
!alt.includes('foto') &&
!alt.includes('imagen') &&
!alt.includes('ilustración');
if (tieneContenidoTexto) {
console.warn(`Posible imagen de texto: ${src} - Alt: "${alt}"`);
}
}
}
});
test('los encabezados son elementos de texto reales', async ({ page }) => {
await page.goto('/');
// Todos los h1-h6 deben ser texto real, no imágenes
const encabezados = page.locator('h1, h2, h3, h4, h5, h6');
const count = await encabezados.count();
for (let i = 0; i < count; i++) {
const encabezado = encabezados.nth(i);
const innerHTML = await encabezado.innerHTML();
// No debe contener etiquetas img
expect(innerHTML).not.toMatch(/<img/i);
// Debe tener contenido de texto real
const textContent = await encabezado.textContent();
expect(textContent?.trim().length).toBeGreaterThan(0);
}
});
test('los botones contienen texto no imágenes', async ({ page }) => {
await page.goto('/');
const botones = page.locator('button, [role="button"], .btn');
const count = await botones.count();
for (let i = 0; i < count; i++) {
const boton = botones.nth(i);
const innerHTML = await boton.innerHTML();
// Permitir iconos SVG, pero no etiquetas img para texto
const tieneEtiquetaImg = /<img[^>]*alt=["'][^"']{10,}["']/i.test(innerHTML);
if (tieneEtiquetaImg) {
console.warn('El botón puede contener imagen de texto:', innerHTML);
}
// Debe tener nombre accesible (texto o aria-label)
const textContent = await boton.textContent();
const ariaLabel = await boton.getAttribute('aria-label');
expect(textContent?.trim() || ariaLabel).toBeTruthy();
}
});
test('el texto decorativo usa CSS no imágenes', async ({ page }) => {
await page.goto('/');
// Verificar elementos que podrían ser texto estilizado
const elementosEstilizados = page.locator('.texto-gradiente, .decorativo, .texto-elegante, [class*="efecto-texto"]');
const count = await elementosEstilizados.count();
for (let i = 0; i < count; i++) {
const elemento = elementosEstilizados.nth(i);
const tagName = await elemento.evaluate(el => el.tagName);
// No debe ser img
expect(tagName).not.toBe('IMG');
}
});
});
Lista de Verificación Resumen
- [ ] Todo el contenido de texto usa elementos HTML de texto real
- [ ] Las fuentes personalizadas usan @font-face o servicios de web fonts
- [ ] Los efectos de texto se logran con CSS (gradientes, sombras, contornos)
- [ ] Los banners y CTAs usan HTML/CSS, no imágenes
- [ ] Solo los logos contienen imágenes de texto esenciales
- [ ] El texto SVG incluye title/aria-label apropiados
- [ ] El texto escala correctamente al hacer zoom
- [ ] El texto se puede seleccionar y copiar
- [ ] Los lectores de pantalla leen el texto naturalmente
Artículos Relacionados
- Guía de Texto Alternativo - Accesibilidad de imágenes
- Hub de Cumplimiento WCAG - Todos los evaluadores de accesibilidad
Referencias
- W3C - WCAG 2.2 SC 1.4.5 Images of Text
- W3C - C22 Using CSS to control visual presentation of text
- WebAIM - Alternative Text
- MDN - Web fonts
- W3C - G140 Separating information and structure from presentation
- MDN - CSS Text
- Google Fonts - Using web fonts