View contents
Introduction
WCAG 1.4.5 (Images of Text) requires that if the technologies being used can achieve the visual presentation, text is used to convey information rather than images of text. This means using real HTML text with CSS styling instead of embedding text within images.
While images of text might seem like an easy way to achieve specific typography, they create significant accessibility barriers and usability problems for many users.
What WCAG Requires
1.4.5 Images of Text (Level AA)
If the technologies being used can achieve the visual presentation, text is used to convey information rather than images of text.
Exceptions:
- Customizable: The image can be visually customized to the user’s requirements
- Essential: A particular presentation of text is essential (like logos or branding)
This means:
- Use CSS to style text instead of creating text as images
- Logos containing text are allowed
- Text essential for conveying information (like brand names) may be images
Who Benefits
| User Type | Benefit |
|---|---|
| Users with low vision | Can enlarge text without pixelation |
| Screen reader users | Text is read naturally, not as “image” |
| Users who need high contrast | Can apply custom stylesheets |
| Users with dyslexia | Can change fonts for readability |
| Search engines | Can index and understand content |
| All users | Text loads faster, scales better |
Problems with Images of Text
1. Cannot Be Resized
<!-- BAD: Image text becomes pixelated when enlarged -->
<img src="heading-text.png" alt="Welcome to Our Site">
<!-- GOOD: Real text scales smoothly -->
<h1 class="custom-heading">Welcome to Our Site</h1>
2. Cannot Be Customized
Users with visual impairments often use browser settings or extensions to:
- Change font sizes
- Apply high contrast themes
- Use specific fonts (like OpenDyslexic)
- Adjust line spacing
Images of text cannot respond to these user preferences.
3. Slow to Load
<!-- Image of text: 50-200KB per image -->
<img src="large-decorative-heading.png" alt="Special Offer">
<!-- Real text with CSS: <1KB -->
<h2 class="decorative-heading">Special Offer</h2>
4. Accessibility Issues
- Screen readers announce “image” rather than reading naturally
- Cannot be selected or copied
- Translation tools cannot process the text
- Cannot be searched by in-page search (Ctrl+F)
Common Scenarios
Acceptable Uses of Images of Text
Logos and Branding
<!-- Acceptable: Logo contains essential brand presentation -->
<img src="company-logo.svg" alt="Acme Corporation">
Diagrams with Essential Text
<!-- Acceptable: Text is part of diagram's essential meaning -->
<figure>
<img src="flowchart.png" alt="Process flowchart showing steps...">
<figcaption>Figure 1: Approval process workflow</figcaption>
</figure>
Unacceptable Uses
Headlines and Body Text
<!-- BAD: Using image for headline -->
<img src="headline.png" alt="Our Services">
<!-- GOOD: Real text with custom styling -->
<h2 class="services-heading">Our Services</h2>
Buttons and Navigation
<!-- BAD: Image button -->
<button><img src="submit-button.png" alt="Submit"></button>
<!-- GOOD: Styled text button -->
<button class="submit-btn">Submit</button>
Call-to-Action Sections
<!-- BAD: Banner image with text -->
<img src="sale-banner.png" alt="50% Off Everything - Shop Now">
<!-- GOOD: HTML with CSS styling -->
<section class="sale-banner">
<h2>50% Off Everything</h2>
<a href="/shop" class="cta-button">Shop Now</a>
</section>
CSS Alternatives to Images of Text
Custom Fonts
/* Use web fonts for custom typography */
@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;
}
Text Effects
/* Gradient text */
.gradient-text {
background: linear-gradient(45deg, #e66465, #9198e5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Text shadow for depth */
.shadow-text {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
/* Outlined text */
.outline-text {
-webkit-text-stroke: 2px #000;
color: transparent;
}
Responsive Typography
/* Text that scales with viewport */
.hero-heading {
font-size: clamp(2rem, 5vw, 5rem);
font-weight: 700;
letter-spacing: -0.02em;
}
Testing for Images of Text
Quick Manual Test
- Right-click text on the page
- If “Save image” or “Copy image” appears, it’s likely an image of text
- Try selecting the text - if you can’t highlight it, it may be an image
- Use browser’s “Inspect Element” to verify if it’s
<img>or text
What to Verify
- [ ] Headlines use real
<h1>-<h6>elements, not images - [ ] Body content is real text, not images
- [ ] Buttons contain text, not image labels
- [ ] Decorative text effects use CSS, not images
- [ ] Logos are the only images containing text (and have proper alt text)
Tools for Testing
- WAVE: Highlights images and their alt text
- axe DevTools: Reports images of text issues
- Browser DevTools: Inspect elements to verify text vs images
- Screen reader: Listen for natural text reading vs “image” announcements
Best Practices Summary
| Do | Don’t |
|---|---|
| Use CSS for text styling | Create images with text |
| Use web fonts for custom typography | Screenshot text for unique fonts |
| Style buttons with CSS | Use image buttons |
| Use SVG icons with separate text | Bake text into icons |
| Keep logos as only text images | Use images for headlines |
Implementation
Web Fonts Implementation
Google Fonts Integration
<!-- In <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">
/* Usage */
.decorative-heading {
font-family: 'Playfair Display', Georgia, serif;
font-weight: 700;
font-size: 3rem;
}
Self-Hosted Fonts
/* Font-face declaration for custom fonts */
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brandfont.woff2') format('woff2'),
url('/fonts/brandfont.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap; /* Prevents invisible text during load */
}
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brandfont-bold.woff2') format('woff2'),
url('/fonts/brandfont-bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Variable font (single file, multiple weights) */
@font-face {
font-family: 'BrandFontVariable';
src: url('/fonts/brandfont-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
CSS Text Effects
Gradient Text
/* Gradient fill on text */
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent; /* Fallback */
}
/* Animated gradient */
.animated-gradient-text {
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: gradient-shift 8s ease infinite;
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
Text Shadows
/* Subtle shadow for depth */
.subtle-shadow {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Bold shadow */
.bold-shadow {
text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
}
/* Glowing text */
.glow-text {
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);
}
/* Neon effect */
.neon-text {
color: #fff;
text-shadow:
0 0 5px #fff,
0 0 10px #fff,
0 0 20px #0ff,
0 0 30px #0ff,
0 0 40px #0ff;
}
/* Retro 3D effect */
.retro-3d {
color: #f5f5f5;
text-shadow:
1px 1px 0 #c4c4c4,
2px 2px 0 #b4b4b4,
3px 3px 0 #a4a4a4,
4px 4px 0 #949494;
}
Text Stroke/Outline
/* Outlined text */
.outlined-text {
-webkit-text-stroke: 2px #333;
color: transparent;
}
/* Filled with stroke */
.stroked-filled {
-webkit-text-stroke: 1px #000;
color: #fff;
}
/* Double outline effect */
.double-outline {
color: #fff;
-webkit-text-stroke: 3px #000;
paint-order: stroke fill;
}
Letter Spacing and Transforms
/* Spaced uppercase (good for headings) */
.spaced-caps {
text-transform: uppercase;
letter-spacing: 0.2em;
font-weight: 600;
}
/* Small caps */
.small-caps {
font-variant: small-caps;
letter-spacing: 0.05em;
}
/* Condensed tracking */
.tight-tracking {
letter-spacing: -0.02em;
}
Decorative Typography Components
React Component
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>
);
}
// Usage
<DecorativeText variant="gradient" as="h1" className="text-5xl font-bold">
Beautiful Heading
</DecorativeText>
Vue 3 Component
<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>
Converting Images of Text to CSS
Before: Image Banner
<!-- OLD: Image with text baked in -->
<img src="sale-banner.png" alt="Summer Sale - Up to 50% Off - Shop Now">
After: CSS Banner
<section class="sale-banner">
<div class="banner-content">
<p class="banner-eyebrow">Limited Time</p>
<h2 class="banner-heading">Summer Sale</h2>
<p class="banner-discount">Up to <span class="highlight">50% Off</span></p>
<a href="/sale" class="banner-cta">Shop Now</a>
</div>
</section>
.sale-banner {
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%);
padding: 3rem;
text-align: center;
border-radius: 1rem;
}
.banner-eyebrow {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 0.5rem;
}
.banner-heading {
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-discount {
font-size: 1.5rem;
color: #fff;
}
.highlight {
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);
}
SVG Text for Complex Effects
<!-- SVG text with effects not possible in CSS -->
<svg viewBox="0 0 400 100" class="svg-heading" aria-labelledby="svg-title">
<title id="svg-title">Creative Typography</title>
<defs>
<linearGradient id="text-gradient" 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="shadow">
<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(#text-gradient)"
filter="url(#shadow)"
font-family="Georgia, serif"
font-size="48"
font-weight="bold"
>
Creative Typography
</text>
</svg>
.svg-heading {
width: 100%;
max-width: 400px;
height: auto;
}
/* SVG text inherits font from CSS if font not specified */
.svg-heading text {
font-family: inherit;
}
Automated Testing
Playwright Tests for Images of Text
import { test, expect } from '@playwright/test';
test.describe('Images of Text Compliance', () => {
test('no images contain text except logos', async ({ page }) => {
await page.goto('/');
// Get all images
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') || '';
// Skip logo images (acceptable)
const isLogo = className.includes('logo') ||
src.includes('logo') ||
alt.toLowerCase().includes('logo');
if (!isLogo) {
// Check if alt text suggests this is text content
const hasTextContent = /^[A-Z]/.test(alt) && // Starts with capital
alt.length > 20 && // Long enough to be a sentence
!alt.includes('photo') &&
!alt.includes('image') &&
!alt.includes('illustration');
if (hasTextContent) {
console.warn(`Potential image of text: ${src} - Alt: "${alt}"`);
}
}
}
});
test('headings are real text elements', async ({ page }) => {
await page.goto('/');
// All h1-h6 should be actual text, not images
const headings = page.locator('h1, h2, h3, h4, h5, h6');
const count = await headings.count();
for (let i = 0; i < count; i++) {
const heading = headings.nth(i);
const innerHTML = await heading.innerHTML();
// Should not contain img tags
expect(innerHTML).not.toMatch(/<img/i);
// Should have actual text content
const textContent = await heading.textContent();
expect(textContent?.trim().length).toBeGreaterThan(0);
}
});
test('buttons contain text not images', async ({ page }) => {
await page.goto('/');
const buttons = page.locator('button, [role="button"], .btn');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const button = buttons.nth(i);
const innerHTML = await button.innerHTML();
// Allow SVG icons, but not img tags for text
const hasImgTag = /<img[^>]*alt=["'][^"']{10,}["']/i.test(innerHTML);
if (hasImgTag) {
console.warn('Button may contain image of text:', innerHTML);
}
// Should have accessible name (text or aria-label)
const textContent = await button.textContent();
const ariaLabel = await button.getAttribute('aria-label');
expect(textContent?.trim() || ariaLabel).toBeTruthy();
}
});
test('decorative text uses CSS not images', async ({ page }) => {
await page.goto('/');
// Check for elements that might be styled text
const styledElements = page.locator('.gradient-text, .decorative, .fancy-text, [class*="text-effect"]');
const count = await styledElements.count();
for (let i = 0; i < count; i++) {
const element = styledElements.nth(i);
const tagName = await element.evaluate(el => el.tagName);
// Should not be img
expect(tagName).not.toBe('IMG');
}
});
});
Summary Checklist
- [ ] All text content uses real HTML text elements
- [ ] Custom fonts use @font-face or web font services
- [ ] Text effects achieved with CSS (gradients, shadows, outlines)
- [ ] Banners and CTAs use HTML/CSS, not images
- [ ] Only logos contain essential images of text
- [ ] SVG text includes proper title/aria-label
- [ ] Text scales properly when zoomed
- [ ] Text can be selected and copied
- [ ] Screen readers read text naturally
Related Articles
- Alt Text Guide - Image accessibility
- WCAG Compliance Hub - All accessibility evaluators
References
- 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