View contents
Images of Text Implementation Guide: CSS Techniques for Accessible Typography
Introduction
Modern CSS provides powerful tools to achieve virtually any typographic effect without resorting to images of text. This guide covers implementation patterns for custom fonts, text effects, and decorative typography that maintain accessibility while achieving sophisticated visual designs.
Using CSS for text presentation ensures users can customize their experience while search engines and assistive technologies can properly interpret content.
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
References
- W3C - WCAG 2.2 SC 1.4.5 Images of Text
- W3C - C22 Using CSS to control visual presentation of text
- W3C - G140 Separating information and structure from presentation
- MDN - CSS Text
- Google Fonts - Using web fonts