Google AdSense Is Breaking Web Accessibility — and Nobody’s Talking About It

We recently added WCAG 2 AA accessibility testing to our CI pipeline at GreenRobot Job Search using pa11y. After fixing dozens of real issues in our own code – contrast ratios, missing form labels, empty anchor tags – we got every page on our site to zero accessibility errors.

Then we opened a pull request.

Our GitHub Actions CI pipeline ran pa11y against the live site – and every single page with Google AdSense failed.

The Problem

Google AdSense injects iframes into your page at runtime with no title attribute:

[iframe src="https://www.google.com/recaptcha/api2/aframe"
        width="0" height="0"
        style="display: none;">[/iframe>

[iframe id="google_esf" name="google_esf"
        src="https://googleads.g.doubleclick.net/pagead/html/..."
        style="display: none;">[/iframe>

(note less than sign replaced with [ so it renders code in the blog. gotta fix this one day)

This violates WCAG 2.4.1 (Bypass Blocks) and WCAG 4.1.2 (Name, Role, Value). Every iframe needs a non-empty title attribute so screen reader users can identify its purpose.

The fix would be trivial on Google’s end. Something like:

[iframe title="Google ad services" ...>[/iframe>

That’s it. One attribute.

Why This Matters

If you’re a developer trying to make your site accessible – and you should be – Google’s ad scripts will fail your automated accessibility tests through no fault of your own. You’re left with two options:

  1. Exclude Google’s iframes from your tests (what we had to do)
  2. Remove ads from your site

Neither option is great. The first means you’re sweeping a real accessibility violation under the rug. The second means giving up revenue because a trillion-dollar company couldn’t add a title attribute to an iframe.

Here’s the pa11y ignore rule we had to add to our CI config:

{
  "defaults": {
    "hideElements": "iframe[src*='google'], iframe[src*='doubleclick'], iframe[src*='recaptcha']"
  }
}

We shouldn’t have to do this.

The Bigger Picture

Google has published extensive accessibility guidelines and Chrome DevTools has built-in accessibility auditing via Lighthouse. Google literally built tools to catch this exact problem. Yet their own ad platform ships inaccessible markup to millions of websites.

This isn’t just a Google problem. The entire ad-tech ecosystem largely ignores accessibility. Ad iframes routinely lack titles, ad content rarely meets contrast requirements, and interactive ad elements often aren’t keyboard-navigable. But Google sets the standard. If AdSense shipped accessible markup, the industry would follow.

A Call to Action

To Google: Please add title attributes to the iframes your ad scripts inject. It’s a one-line fix that would instantly improve accessibility across millions of websites.

To ad-tech competitors: There’s a real opportunity here. If you’re building an ad platform that competes with AdSense, ship accessible markup by default. Make it a selling point. As accessibility regulations tighten globally – the European Accessibility Act took effect in June 2025 – publishers will increasingly need ad partners that don’t break their compliance.

To fellow developers: Don’t let third-party scripts be an excuse to skip accessibility testing. Add the ignore rules you need to keep your CI green, but document why those rules exist. File bugs with the offending services. And keep testing the code you can control.

We got our site from 91 accessibility errors down to zero across 16 pages. The only failures left are Google’s, not ours. We’ll keep the ignore rules in place for now, but we’d love nothing more than to remove them.


Andy Triboletti is the founder of GreenRobot. GreenRobot Job Search helps developers find jobs at VC-backed companies. Our codebase is tested with pa11y (WCAG 2 AA), Nu HTML Checker, and Puppeteer console error detection on every pull request.

How to Build Fast-Loading Pages: A Practical Guide (+ New CodeFrog Feature)

Why Page Speed Matters

Page load time directly impacts user experience, SEO rankings, and conversion rates. Google has made Core Web Vitals a ranking factor, and studies consistently show that users abandon pages that take more than 3 seconds to load. Every 100ms of added latency costs measurable engagement.

The single biggest contributor to slow pages? Oversized images and unoptimized resources.

The Problem We Found on Our Own Site

We recently ran our own Mega Report against codefrog.app and discovered our landing page was shipping 3.24 MB of images to users. The breakdown:

  • codefrog-1024-transparent.png – 1.10 MB (hero logo, displayed at 200px)
  • owasp.png – 594 KB (screenshot, 2880×1800 source)
  • githubpr.png – 410 KB (screenshot, 2880×1800 source)
  • megareport.png – 322 KB (screenshot, 2880×1800 source)
  • Plus 4 more oversized screenshots

The hero logo was a 1024×1024 PNG being displayed at a maximum of 200px CSS width. The screenshots were all 2880×1800 retina captures being served at full resolution regardless of display size.

How We Fixed It

1. Right-Size Your Images

Never serve an image larger than it needs to be. If your CSS displays an image at 200px wide, a 512px source is plenty (accounts for 2x retina). If your screenshots display in a grid at ~700px, a 1440px source covers retina displays.

We resized our hero logo from 1024px to 512px and all screenshots from 2880×1800 to 1440×900.

2. Use WebP Format

WebP provides significantly better compression than PNG for photographic and screenshot content, while still supporting transparency. Browser support is now above 97%.

Our results after converting to WebP:

Total page image weight dropped from 3.24 MB to ~248 KB – a 92% reduction.

3. Use the <picture> Element for Fallback

The <picture> element lets you serve WebP to browsers that support it while falling back to PNG for older browsers.

4. Lazy Load Below-the-Fold Images

Add loading="lazy" to any image that isn’t visible in the initial viewport. The browser will defer loading these images until the user scrolls near them, dramatically improving initial page load time.

For your hero/above-the-fold image, use fetchpriority="high" instead to tell the browser to prioritize it.

5. Quick Optimization with Command-Line Tools

You don’t need fancy build pipelines. On macOS, you can optimize images with tools you already have:

# Resize with sips (built into macOS)
sips -z 900 1440 screenshot.png

# Convert to WebP with ImageMagick
magick screenshot.png -resize 1440x900 -quality 80 screenshot.webp

Common Thresholds for Page Weight

Based on industry best practices, here are reasonable targets:

Metric Good Needs Work Poor
Total page weight < 1.5 MB 1.5 – 3 MB > 3 MB
Single image < 200 KB 200 KB – 500 KB > 500 KB
Single resource < 1 MB 1 – 5 MB > 5 MB
Total images < 1 MB 1 – 3 MB > 3 MB

PNG images over 100 KB are almost always candidates for WebP conversion.

New in CodeFrog: Page Size & Performance Test

We built these insights directly into CodeFrog’s Mega Report. The new Page Size & Performance test, coming in an upcoming release, automatically:

  • Discovers all page resources – images, scripts, stylesheets, fonts, and other assets
  • Measures compressed and uncompressed sizes for each resource
  • Flags oversized resources with severity ratings:
    • Critical: Any single resource over 5 MB, or total page over 10 MB
    • High: Any image over 1 MB, or total page over 5 MB
    • Medium: Any image over 500 KB, or total page over 3 MB
    • Low: Any image over 200 KB, or total page over 1.5 MB
    • Info: PNG images over 100 KB that could benefit from WebP conversion
  • Shows a breakdown by resource type (images, scripts, styles, fonts)
  • Shows compression savings so you can see how much transfer encoding helps
  • Works in Sitemap Mode to audit every page across your entire site

The Page Size test integrates with the existing Mega Report alongside Accessibility, Security, SEO, Meta Tags, and HTML Validation tests. You get a single comprehensive report covering all aspects of your site’s health.

How It Looks

When you run a Mega Report, the Page Size section shows:

  1. Summary card with total page weight, compression savings percentage, resource count, and breakdown by type
  2. Sorted resource list showing every asset on the page, color-coded by size, with file type icons
  3. Severity counts that feed into the overall health score grade (A through F)

Export Support

Page Size results are included in both Markdown and PDF exports, so you can share findings with your team or include them in client reports.

Try It Yourself

The Page Size & Performance test will be available in the next release of CodeFrog for macOS and Windows. In the meantime, you can manually audit your pages using browser DevTools (Network tab, sort by size) or run Lighthouse.

The fastest page is the one that loads the least. Start with your images – they’re almost always the biggest win.


CodeFrog is a developer tool for macOS and Windows that helps you find and fix bugs with comprehensive code analysis, web testing, and GitHub automation. Learn more at codefrog.app.

moes folks in video but my prompt didnt include moes folks

I created a video with ai. I didn’t mention Moes Folks when creating but I used to be on a robot team called moe and it is in video.

MOE #365 US FIRST (Miracle of Engineering) was a high school robot team with a green robot. I own greenrobot.com acquired after. Anyone have other weird stuff happen with AI, specifically Google Gemini video generation veo? I generated this video for my app codefrog. Prompt attached.

https://youtu.be/TWZB1q3iMeU?si=9rPvMgAtzNOHDJWx

Server misconfiguration error corrected

I got an anonymous email this morning. I was still using TLS v1 on greenrobot.com. Using openai and ssllabs I was able to correct the problem in a few minutes, no new packages needed. Just editing a file. I considered using Cloudflare, which I have not used yet, but went with fixing it at the source vs signing up for Cloudflare and reconfiguring DNS at registrar. Would I want to do this kind of Apache work for someone else? Not really, it stressed me out. But also, yeah I’d do it.

Does this impact users at all? I am pretty sure it does not impact any of the users of any of my apps. If you are using very old phones or computers you may no longer be able to connect to some of my apps on greenrobot.com, but anything recent should be fine. Let me know in an email if anything is wrong. andy@greenrobot.com

Thank you for your time reading this.

ADA Web Lawsuit Trends for 2026: What 2025 Filings Reveal | UsableNet, Inc.

https://blog.usablenet.com/ada-web-lawsuit-trends-2026

“These industries share a common complaint. Their websites are the primary way customers access essential services. When core workflows such as purchasing, ordering, booking, or refilling prescriptions are inaccessible, litigation follows quickly.”

I cleaned up some of the accessibility and html validation errors on this site using codefrog.app

It now has a ‘C’ rating for the index page from CodeFrog.app mega report rating. Gotta start somewhere. Email me at andy@greenrobot.com if you’d like me to run a report on your site including security, seo, and accessibility tests. Download for macOS at https://codefrog.app. Windows coming soon.

Happy New Year.

My year of development in 2025

My year of development in 2025. I had a job up until May. Currently working on codefrog.app for Windows and searching for work and people to develop stuff with.

I created a lot of sites and apps using AI (Cursor, CodeRabbit, AugmentCode, Antigravity, Amp)

march
3dtankbattle.com

3dwebgames.com

may
longevity.greenrobot.com

aicareers.greenrobot.com

launchday.greenrobot.com

mentalhealthlawyers.greenrobot.com

remotedevjobs.greenrobot.com

june
robots.greenrobot.com

september
gunstopperdrone.com

game.gunstopperdrone.com

december
codefrog.app

github.com/greenrobotllc/bio-neighbor

Notable accomplishment: Getting CodeFrog.app approved on the App Store and being able to use open-source programs installed on the computer within the app while still sandboxed by utilizing SSH on the localhost. Thank you, Apple, for approving it! I am so grateful!

Unfortunate setback: My loving, kind, good watchdog, Albert, got cancer, and we have an appointment with radiation upcoming in January.

Kotlin Multiplatform vs Flutter: A Deep Dive into Async Programming

Introduction

As a developer who has built a released application in Flutter, I’ve recently started exploring Kotlin Multiplatform (KMP) for a new project. After building codefrog.app – a macOS developer tool in Flutter – I’ve been evaluating KMP for similar functionality. Even in the early stages, the differences in how these frameworks handle asynchronous programming are immediately apparent. This article explores why KMP’s async programming model appears superior, especially for native platform integration.

The Core Difference: Coroutines vs Isolates

Flutter’s Isolate Model

Flutter uses Isolates for concurrent execution. Isolates are separate memory spaces that communicate via message passing – essentially separate processes that can’t share memory.

// Flutter isolate example
import 'dart:isolate';

void isolateFunction(SendPort sendPort) {
  // Heavy computation
  sendPort.send(result);
}

void main() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(isolateFunction, receivePort.sendPort);
  var result = await receivePort.first;
}

Key Limitations:

  1. No Shared Memory: Isolates can’t share objects – everything must be serialized
  2. Message Passing Overhead: All communication requires serialization/deserialization
  3. Platform API Restrictions: Isolates can’t directly access many platform APIs
  4. Complex State Management: Sharing state between isolates requires explicit message passing

KMP’s Coroutine Model

KMP uses Kotlin Coroutines – lightweight threads that can suspend and resume without blocking.

// KMP coroutine example
suspend fun fetchData(): Data {
    return withContext(Dispatchers.IO) {
        // Network call - suspends, doesn't block
        api.getData()
    }
}

// Usage
viewModelScope.launch {
    val data = fetchData() // Seamless async/await
    updateUI(data)
}

Key Advantages:

  1. Shared Memory: Coroutines share the same memory space
  2. Zero Serialization Overhead: Direct object access
  3. Full Platform API Access: Can call any platform API directly
  4. Structured Concurrency: Automatic cancellation and resource management

The Secure Storage Problem

Flutter’s Isolate Limitation

While building codefrog.app, I discovered that isolates cannot access secure storage APIs on macOS. This became a significant issue when porting to Windows, where the app would hang – a problem that doesn’t occur on macOS. I’m still working through the multithreading issues on Windows, which has proven to be very time-consuming.

// This DOESN'T work in an isolate
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

void isolateFunction() {
  final storage = FlutterSecureStorage();
  // ❌ CRASHES: Secure storage requires main isolate
  await storage.write(key: 'token', value: 'secret');
}

Why This Happens:

  • macOS Keychain access requires the main thread/isolate
  • Isolates run in separate processes without proper entitlements
  • Secure storage plugins are designed for the main isolate only
  • Workarounds require complex message passing back to main isolate

I discocered the macOS app had no problems doing everything on the main thread not using isoaltes, but when trying to port to Windows it hung, meaning I would have to use isolates on Windows in order to release a Windows Version using Flutter.

KMP’s Seamless Access

In KMP, coroutines can access secure storage directly because they share the same memory space and thread context.

// This WORKS perfectly in a coroutine
suspend fun saveCredentials(token: String) {
    withContext(Dispatchers.Main) {
        // Direct Keychain access - no restrictions
        keychain.save("token", token)
    }
}

// Can be called from any coroutine
viewModelScope.launch {
    saveCredentials("secret") // Works everywhere!
}

Why This Works:

  • Coroutines can switch dispatchers (threads) seamlessly
  • Dispatchers.Main gives access to UI and platform APIs
  • No serialization needed – direct object access
  • Full platform entitlements and permissions

macOS Sandbox Restrictions

Flutter’s Sandbox Challenges

macOS sandboxed apps have strict security requirements. Flutter isolates face additional challenges:

// Sandboxed app trying to access file system from isolate
void isolateFunction() async {
  // ❌ May fail: Isolates don't inherit sandbox entitlements properly
  final file = File('/path/to/file');
  await file.writeAsString('data');
}

Issues:

  1. Entitlement Inheritance: Isolates may not properly inherit app entitlements
  2. File System Access: Sandbox file access can be restricted in isolates
  3. Network Permissions: Some network operations require main isolate
  4. User Permissions: System dialogs (file picker, etc.) must be on main isolate

KMP’s Native Integration

KMP coroutines work seamlessly with macOS sandboxing:

// Direct platform API access with proper sandbox support
suspend fun saveFile(data: String) {
    withContext(Dispatchers.Main) {
        // Full sandbox support - uses app's entitlements
        val panel = NSSavePanel()
        if (panel.runModal() == .OK) {
            data.writeToFile(panel.url.path)
        }
    }
}

Advantages:

  • Coroutines use the app’s main thread context
  • All entitlements and permissions are inherited
  • Direct access to NSFileManager, Keychain, etc.
  • No workarounds needed

Window Management: A Performance Nightmare

Flutter’s Engine-Per-Window Model

One of the most painful experiences building codefrog.app was implementing File → New Window. Flutter’s architecture requires a separate engine instance for each window.

// Flutter: Creating a new window
void createNewWindow() {
  // ❌ Must create entire new Flutter engine
  final engine = FlutterEngine();
  await engine.run(); // Expensive initialization
  
  // ❌ Black screen while engine initializes (1-2 seconds)
  final window = await engine.createWindow();
  
  // ❌ Each window has separate memory footprint
  // ❌ No shared state between windows
}

Problems:

  1. Engine Initialization: Each window requires full Flutter engine startup (1-2 seconds)
  2. Black Screen: Visible delay before content appears
  3. Memory Overhead: Each engine instance consumes significant memory
  4. State Isolation: Windows can’t easily share state
  5. Complex Implementation: Requires platform channel setup for each window

Real Experience from codefrog.app:

  • New windows took 1-2 seconds to appear
  • Black screen was visible to users (poor UX)
  • Memory usage increased linearly with window count
  • Had to implement complex state synchronization between windows
  • File menu → New Window required significant platform channel work

KMP’s Native Window Management

In KMP with SwiftUI, window creation is trivial and instant:

// KMP/SwiftUI: Creating a new window
func openNewWindow() {
    let window = NSWindow(
        contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
        styleMask: [.titled, .closable, .resizable],
        backing: .buffered,
        defer: false
    )
    
    window.contentView = NSHostingView(rootView: ContentView())
    window.makeKeyAndOrderFront(nil)
    // ✅ Instant - no engine initialization
    // ✅ No black screen
    // ✅ Shared framework instance
}

Advantages:

  1. Instant Creation: Windows appear immediately (milliseconds)
  2. No Black Screen: Content is ready before window appears
  3. Shared Framework: KMP framework is loaded once, shared across windows
  4. Native Performance: Uses platform-native windowing APIs
  5. Simple Implementation: ~10 lines of code vs. hundreds in Flutter

Real Experience:

  • New windows appear instantly
  • No visible delay or black screen
  • Memory overhead is minimal (shared framework)
  • State can be easily shared via shared ViewModels
  • File menu → New Window: 5 minutes to implement vs. hours in Flutter

Async Programming Comparison

Flutter: Futures and Streams

Flutter uses Dart’s Future and Stream for async operations:

// Flutter async
Future<List<Data>> fetchData() async {
  final response = await http.get(url);
  return parseData(response.body);
}

// Stream for reactive updates
Stream<Progress> generateProgress() async* {
  yield Progress.started;
  await Future.delayed(Duration(seconds: 1));
  yield Progress.processing(50);
  yield Progress.completed;
}

Issues:

  1. Isolate Restrictions: Can’t use platform APIs in isolates
  2. Serialization Overhead: Passing data between isolates requires serialization
  3. Complex Error Handling: Errors must be serialized across isolates
  4. No Structured Concurrency: Manual cancellation and cleanup

KMP: Coroutines and Flow

KMP uses Kotlin Coroutines and Flow:

// KMP async
suspend fun fetchData(): List<Data> {
    return withContext(Dispatchers.IO) {
        val response = client.get(url)
        parseData(response.body)
    }
}

// Flow for reactive updates
fun generateProgress(): Flow<Progress> = flow {
    emit(Progress.Started)
    delay(1000)
    emit(Progress.Processing(50))
    emit(Progress.Completed)
}

Advantages:

  1. Full Platform Access: Can use any platform API from coroutines
  2. Zero Serialization: Direct object access, no overhead
  3. Structured Concurrency: Automatic cancellation and resource management
  4. Seamless Bridging: Coroutines bridge to Swift async/await automatically

Performance Comparison

Memory Usage

Flutter:

  • Each window: ~50-100MB (separate engine)
  • Isolates: Additional memory per isolate
  • Total for 3 windows: ~200-300MB

KMP:

  • Shared framework: ~20-30MB (loaded once)
  • Each window: ~5-10MB (just UI)
  • Total for 3 windows: ~35-60MB

Window Creation Time

Flutter:

  • Engine initialization: 1-2 seconds
  • Black screen visible: Yes
  • User-perceived delay: High

KMP:

  • Window creation: <50ms
  • Black screen visible: No
  • User-perceived delay: None

Async Operation Overhead

Flutter:

  • Isolate communication: Serialization overhead
  • Message passing: ~1-5ms per message
  • State synchronization: Complex and error-prone

KMP:

  • Coroutine context switch: <1ms
  • Direct memory access: Zero overhead
  • State sharing: Native Swift/Kotlin objects

Real-World Use Case: codefrog.app

The Flutter Experience

Building codefrog.app in Flutter revealed several pain points:

  1. Secure Storage: Isolates can’t access Keychain – had to use workarounds like SSH localhost for macOS sandbox builds (works fine on main thread)
  2. New Windows: 1-2 second delay, black screen, poor UX
  3. State Management: Complex synchronization between windows
  4. Platform Integration: Limited by isolate restrictions
  5. Performance: Higher memory usage, slower window creation
  6. Windows Porting: App hangs on Windows due to multithreading issues – still working on this complex problem

Early KMP Exploration

While just getting started with KMP, the initial experience shows promise:

  1. Secure Storage: Direct Keychain access appears straightforward, no workarounds needed
  2. New Windows: Instant creation in initial testing, no delay, native feel
  3. State Management: Shared ViewModels look simple and efficient
  4. Platform Integration: Full access to all macOS APIs without restrictions
  5. Performance: Lower memory footprint and faster operations in early benchmarks

Conclusion: Why KMP Appears Superior for Async Programming

Based on my experience building production apps in Flutter and early exploration of KMP, KMP’s async programming model appears significantly better for native platform integration:

Technical Superiority

  1. Coroutines vs Isolates: Coroutines provide better performance, lower overhead, and full platform access
  2. Memory Model: Shared memory eliminates serialization overhead
  3. Platform Integration: Direct access to all platform APIs without restrictions
  4. Structured Concurrency: Automatic resource management and cancellation

Developer Experience

  1. Simplicity: Easier to reason about and debug
  2. Performance: Faster operations, lower memory usage
  3. Native Feel: Windows and UI feel truly native
  4. Less Code: Simpler implementations for complex features

User Experience

  1. Performance: Instant window creation, no delays
  2. Responsiveness: Smooth async operations
  3. Native Feel: Feels like a true macOS app
  4. Reliability: Fewer edge cases and workarounds

When to Choose Each

Choose Flutter if:

  • You need cross-platform mobile (iOS + Android) with web
  • You’re building a consumer app with simple async needs
  • You don’t need deep platform integration
  • Your team is primarily Dart/Flutter developers

Choose KMP if:

  • You need native desktop apps (macOS, Windows, Linux)
  • You require deep platform integration (Keychain, file system, etc.)
  • Performance and memory usage matter
  • You want true native feel and behavior
  • You’re building developer tools or professional software

Final Thoughts

As someone who has shipped production apps in Flutter and is now exploring KMP, KMP’s async programming model appears to be the clear winner for desktop applications requiring native platform integration. The coroutine model provides better performance, simpler code, and full platform access – things that Flutter’s isolate model simply cannot match.

For codefrog.app and similar developer tools, KMP’s advantages in window management, secure storage access, and overall performance make it an attractive choice. Based on early exploration, it seems the days of waiting 1-2 seconds for new windows and working around isolate limitations could be over.

I’m still in the early stages of my KMP journey, but the initial signs are very promising. The async programming model alone makes it worth serious consideration for any desktop application requiring deep platform integration.


This article is based on real-world experience building codefrog.app in Flutter and early exploration of Kotlin Multiplatform. Performance numbers and observations are from actual Flutter development and initial KMP testing.