# Building WCAG-Compliant Flutter Components: Automating Contrast Calculations

While building Hux UI, I kept running into the same accessibility problem: buttons with custom colors that had terrible text contrast. You'd pick a nice brand color, slap white text on it, and suddenly your button was unreadable.

Instead of manually checking contrast ratios every time, I decided to build a system that handles this automatically. Here's how I implemented WCAG-compliant contrast calculations in Flutter.

## **The Problem with Manual Contrast Checking**

Most design systems punt on this problem. They give you preset color combinations that work, but the moment you use a custom brand color, you're on your own.

```plaintext
// This could be completely unreadable
Container(
  color: myBrandColor, // Could be any color
  child: Text('Button Text', style: TextStyle(color: Colors.white)),
)
```

## **The WCAG Algorithm**

WCAG defines contrast ratio as the luminance difference between two colors. The formula is:

```plaintext
contrast = (lighter + 0.05) / (darker + 0.05)
```

Where luminance is calculated using the sRGB color space with gamma correction. For normal text, you need at least 4.5:1 contrast ratio for AA compliance.

## **Implementation in Flutter**

Here's how I implemented the contrast calculation system:

```plaintext
/// Calculates the relative luminance of a color according to WCAG guidelines
double _getRelativeLuminance(Color color) {
  // Convert RGB values to 0-1 range
  final r = color.r / 255.0;
  final g = color.g / 255.0;
  final b = color.b / 255.0;

  // Apply gamma correction
  final rLinear = r <= 0.03928 ? r / 12.92 : pow((r + 0.055) / 1.055, 2.4);
  final gLinear = g <= 0.03928 ? g / 12.92 : pow((g + 0.055) / 1.055, 2.4);
  final bLinear = b <= 0.03928 ? b / 12.92 : pow((b + 0.055) / 1.055, 2.4);

  // Calculate relative luminance using ITU-R BT.709 coefficients
  return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
}

/// Calculates contrast ratio between two colors
double _calculateContrastRatio(Color color1, Color color2) {
  final luminance1 = _getRelativeLuminance(color1);
  final luminance2 = _getRelativeLuminance(color2);

  final lighter = luminance1 > luminance2 ? luminance1 : luminance2;
  final darker = luminance1 > luminance2 ? luminance2 : luminance1;

  return (lighter + 0.05) / (darker + 0.05);
}
```

The tricky part is the gamma correction. sRGB colors aren't linear - they use a gamma curve to match how displays work. You have to convert to linear RGB before calculating luminance.

## **Automatic Text Color Selection**

Now I can automatically pick the best text color:

```plaintext
Color _getContrastingTextColor(Color backgroundColor, BuildContext context) {
  final whiteContrast = _calculateContrastRatio(
    backgroundColor, 
    HuxTokens.textInvert(context)
  );
  final blackContrast = _calculateContrastRatio(
    backgroundColor, 
    HuxTokens.textPrimary(context)
  );

  // Choose the color with better contrast
  return whiteContrast > blackContrast
      ? HuxTokens.textInvert(context)
      : HuxTokens.textPrimary(context);
}
```

This runs every time a button renders, automatically ensuring WCAG AA compliance regardless of the background color.

## **Semantic Design Tokens**

The other technical challenge was building a theme system that works in both light and dark mode. Instead of hardcoding colors, I built semantic tokens:

```plaintext
class HuxTokens {
  /// Primary text color that adapts to light/dark theme
  static Color textPrimary(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return isDark ? HuxColors.white : HuxColors.black;
  }

  /// Surface color for elevated components
  static Color surfaceElevated(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return isDark ? HuxColors.black70 : HuxColors.white;
  }
}
```

Every component uses these tokens instead of raw colors. When you switch themes, everything adapts automatically without any component-level changes.

## **Performance Considerations**

You might worry about calculating contrast ratios on every render, but:

1. **It's fast**: The math is just basic arithmetic, no expensive operations
    
2. **It's necessary**: The alternative is manually checking every color combination
    
3. **It's cached**: Flutter's widget rebuilding is smart about when this actually runs
    

## **Real-World Usage**

In the actual component, it looks like this:

```plaintext
ButtonStyle _getButtonStyle(BuildContext context) {
  final effectivePrimaryColor = primaryColor ?? HuxTokens.primary(context);
  final foregroundColor = _getContrastingTextColor(effectivePrimaryColor, context);

  return ButtonStyle(
    backgroundColor: WidgetStateProperty.all(effectivePrimaryColor),
    foregroundColor: WidgetStateProperty.all(foregroundColor),
    // ... other styles
  );
}
```

No matter what color you pass in, the text will be readable.

## **Testing the Algorithm**

I tested this against the [WebAIM contrast checker](https://webaim.org/resources/contrastchecker/) to make sure my implementation matches the WCAG spec. The results are identical.

You can test it yourself:

```plaintext
HuxButton(
  primaryColor: Color(0xFF6366F1), // Any color
  child: Text('Always Readable'),
  onPressed: () {},
)
```

## **Why This Matters**

Accessibility shouldn't be an afterthought. By building it into the component layer, developers get WCAG compliance without having to think about it.

The contrast calculation runs automatically, the semantic tokens handle theme switching, and your app stays accessible regardless of design changes.

## **The Trade-offs**

**Pros:**

* Zero cognitive overhead for developers
    
* Guaranteed WCAG AA compliance
    
* Works with any color combination
    
* Adapts to theme changes automatically
    

**Cons:**

* Slight computation cost (negligible in practice)
    
* Less control over exact text colors (though you probably don't want that control)
    

## **Try It Out**

The system is part of [Hux UI](https://pub.dev/packages/hux), but the algorithms are standalone. You could implement this in any Flutter project:

```plaintext
flutter pub add hux
```

Have you implemented accessibility features in your component libraries? I'm curious about other approaches to this problem!

---

🔗 **Links:**

* [Hux UI on pub.dev](https://pub.dev/packages/hux)
    
* [WCAG Contrast Guidelines](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)
    
* [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
