惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
V
Visual Studio Blog
雷峰网
雷峰网
J
Java Code Geeks
博客园 - 三生石上(FineUI控件)
人人都是产品经理
人人都是产品经理
MyScale Blog
MyScale Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
B
Blog RSS Feed
C
Check Point Blog
博客园 - Franky
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
D
Docker
IT之家
IT之家
博客园 - 聂微东
腾讯CDC
U
Unit 42
Microsoft Security Blog
Microsoft Security Blog
The Cloudflare Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Your contrast checker probably lies about rgba() text
Daniel Cheong · 2026-06-24 · via DEV Community

Your contrast checker probably lies about rgba() text. Most checkers — and most hand-rolled WCAG snippets — assume both colours are fully opaque. The moment your text or its background uses rgba() or opacity, the numbers can be off by a full pass/fail. Here's why, and the one extra step that fixes it.

The WCAG math, quickly

The contrast ratio between two colours is:

(L1 + 0.05) / (L2 + 0.05)

where L1 is the relative luminance of the lighter colour and L2 the darker. Relative luminance needs sRGB gamma correction first — you can't just average the RGB channels:

function luminance([r, g, b]) {
  const a = [r, g, b].map((v) => {
    v /= 255;
    return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
}

function ratio(fg, bg) {
  const [l1, l2] = [luminance(fg), luminance(bg)];
  const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
  return (hi + 0.05) / (lo + 0.05);
}

Sanity check: black on white is 21:1, white on white is 1:1. If those two anchors are right, the core is right. AA wants 4.5:1 for normal text and 3:1 for large text (≥ 18pt, or 14pt bold).

So far so standard. Here's the part that bites.

The bug: luminance has no concept of alpha

luminance() takes three numbers. There is no fourth slot for alpha — and there can't be, because a translucent colour has no luminance of its own. What you actually see is the translucent colour blended over whatever is behind it.

Consider muted helper text that a designer wrote as:

color: rgba(0, 0, 0, 0.55);   /* "black at 55%" */
background: #ffffff;

Run the naive check and you'll feed it [0,0,0] vs [255,255,255] and get a triumphant 21:1. But the pixels on screen are not black. They're black composited over white at 55% opacity, which is a medium grey — and its real ratio is closer to 3.7:1. That fails AA for body text. The checker said you passed by 4x. You shipped unreadable text.

The fix: composite first, then measure

Before computing luminance, flatten any translucent colour onto its actual backdrop using the standard "source-over" formula, per channel:

out = fg * alpha + bg * (1 - alpha)

// Flatten a translucent fg over an opaque bg.
function composite([fr, fg_, fb, fa], [br, bg_, bb]) {
  return [
    Math.round(fr * fa + br * (1 - fa)),
    Math.round(fg_ * fa + bg_ * (1 - fa)),
    Math.round(fb * fa + bb * (1 - fa)),
  ];
}

function ratioWithAlpha(fgRgba, bgRgb) {
  const flat = composite(fgRgba, bgRgb);   // real on-screen colour
  return ratio(flat, bgRgb);
}

ratioWithAlpha([0, 0, 0, 0.55], [255, 255, 255]); // ~3.7  ✅ now honest

Now the result matches what a human eye (and an auditor's tooling) actually sees.

Three things that trip people up

1. opacity is not rgba(), and it's worse. opacity: 0.55 on an element fades the text and lets the background show through, but it also composites over every ancestor layer, not just the immediate parent. If there's a background image or a gradient behind the element, there is no single backdrop colour to composite against — the contrast genuinely varies pixel by pixel. The honest move is to avoid opacity for text colour entirely and use a solid colour you can actually measure.

2. A translucent background needs the same treatment. A card with background: rgba(255,255,255,0.8) over a photo isn't white — flatten it over the dominant backdrop colour before checking the text on it.

3. Stacked alphas compound. Translucent text on a translucent panel on a page background means two composites, innermost backdrop first. Resolve from the bottom layer up.

Takeaways

  • The WCAG luminance formula has no alpha input by design — translucent colours must be flattened first.
  • Always composite (fg*a + bg*(1-a)) against the real backdrop before computing the ratio, or your checker reports a colour that never reaches the screen.
  • Prefer solid rgb/hex for text. If you must fade it, pre-compute the flattened colour so design, code, and audit all agree on one value.

I kept re-deriving the compositing step every time a design used faded text, so I keep a small free color contrast checker around that takes alpha into account. Get the two anchor values (21:1 and 1:1) right, remember to flatten, and contrast stops being guesswork.