Color Contrast
Reports the WCAG 2.0 contrast ratio between a base and background colour with an AA/AAA/Fail grade — luminance and ratio computed from hex inputs, or overridden for APCA/pre-computed values.
Preview
<div class="flex flex-col items-start gap-4">
<div class="flex flex-wrap items-center gap-3">
<x-ui.color-contrast base="#0f172a" against="#ffffff" label="Text / Surface" />
<x-ui.color-contrast base="#ffffff" against="#6366f1" label="On primary" />
<x-ui.color-contrast base="#94a3b8" against="#f8fafc" label="Muted" />
</div>
<div class="flex flex-wrap items-center gap-3">
<x-ui.color-contrast variant="icon" base="#0f172a" against="#ffffff" />
<x-ui.color-contrast variant="icon" base="#ffffff" against="#22c55e" />
<x-ui.color-contrast variant="icon" base="#cbd5e1" against="#e2e8f0" />
</div>
</div>
Installation
php artisan ui:add color-contrast
Registry contract
php artisan ui:add color-contrast
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/color-contrast.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Color Contrast (`color-contrast`)
Reports the WCAG 2.0 contrast ratio between a base and background colour with an AA/AAA/Fail grade — luminance and ratio computed from hex inputs, or overridden for APCA/pre-computed values.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add color-contrast
```
## Usage
```blade
<div class="flex flex-col items-start gap-4">
<div class="flex flex-wrap items-center gap-3">
<x-ui.color-contrast base="#0f172a" against="#ffffff" label="Text / Surface" />
<x-ui.color-contrast base="#ffffff" against="#6366f1" label="On primary" />
<x-ui.color-contrast base="#94a3b8" against="#f8fafc" label="Muted" />
</div>
<div class="flex flex-wrap items-center gap-3">
<x-ui.color-contrast variant="icon" base="#0f172a" against="#ffffff" />
<x-ui.color-contrast variant="icon" base="#ffffff" against="#22c55e" />
<x-ui.color-contrast variant="icon" base="#cbd5e1" against="#e2e8f0" />
</div>
</div>
```
## Props
- `base` (mixed|null, default `null`) — Foreground/text colour as a hex string.
- `against` (mixed|null, default `null`) — Background colour it is compared against, as a hex string.
- `ratio` (mixed|null, default `null`) — Explicit contrast ratio override, used instead of computing one from base/against (e.g. for APCA).
- `passes` (mixed|null, default `null`) — Explicit pass/fail or grade override ("AA", "AAA", "fail" or a boolean) instead of deriving it from the ratio.
- `label` (mixed|null, default `null`) — Optional text shown before the ratio, such as a token pairing name.
- `variant` (pill|icon, default `pill`) — pill renders a text grade badge; icon renders a compact pass/fail glyph.
## Use when
- Use to document, inspect, and apply design-system decisions consistently across teams and AI tooling.
- Reporting whether two colours meet a WCAG contrast threshold, such as validating a token pairing in a style guide or design QA.
- Showing a live AA/AAA/Fail grade next to a colour combination without asking the reader to compute the ratio themselves.
## Avoid when
- Do not present token-level controls to end users who only need product functionality.
- Swatching a single colour token on its own; use color-token, which does not compare against a background.
## Anti-patterns
- Exposing implementation tokens as product settings
## Rules
- Use the `<brok:color-contrast>` tag (or `<x-ui.color-contrast>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/color-contrast
- Registry JSON (files, props, contract): https://brokui.dev/r/open/color-contrast.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| base | mixed | null | null | Foreground/text colour as a hex string. |
| against | mixed | null | null | Background colour it is compared against, as a hex string. |
| ratio | mixed | null | null | Explicit contrast ratio override, used instead of computing one from base/against (e.g. for APCA). |
| passes | mixed | null | null | Explicit pass/fail or grade override ("AA", "AAA", "fail" or a boolean) instead of deriving it from the ratio. |
| label | mixed | null | null | Optional text shown before the ratio, such as a token pairing name. |
| variant | pill | icon | pill | pill renders a text grade badge; icon renders a compact pass/fail glyph. |
Slots
default— Not used by this component; present for attribute-bag compatibility.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Computes relative luminance and the WCAG 2.0 ratio from base/against hex colours, or uses an explicit ratio/passes override for APCA or pre-computed values.
- Grades the result AAA (>= 7), AA (>= 4.5) or Fail, and renders either a text pill or a compact pass/fail icon depending on variant.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Inspect and govern design-system decisions.
Use when
- Use to document, inspect, and apply design-system decisions consistently across teams and AI tooling.
- Reporting whether two colours meet a WCAG contrast threshold, such as validating a token pairing in a style guide or design QA.
- Showing a live AA/AAA/Fail grade next to a colour combination without asking the reader to compute the ratio themselves.
Avoid when
- Do not present token-level controls to end users who only need product functionality.
- Swatching a single colour token on its own; use color-token, which does not compare against a background.
Use instead
- Product-level controls for end users
Anti-patterns
- Exposing implementation tokens as product settings
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- The whole control exposes its computed ratio and grade through a single role="img" aria-label, so screen readers get one summarised announcement instead of reading each part separately.
- The swatch's inner glyph and the icon variant's pass/fail glyph are decorative and hidden with aria-hidden.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'base' => null,
'against' => null,
'ratio' => null,
'passes' => null,
'label' => null,
'variant' => 'pill',
])
{{--
Color Contrast (tokenui.dev-inspired) — reports the WCAG 2.0 contrast ratio
between a base (text) colour and the colour it sits `against` (background).
Relative luminance and the ratio are computed from hex inputs in PHP; pass an
explicit `ratio`/`passes` to override (e.g. for APCA or pre-computed values).
`variant="icon"` renders a compact pass/fail glyph instead of a grade pill.
--}}
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$variant = $styles['normalizeVariant']($variant);
$parseHex = static function (?string $hex): ?array {
$hex = ltrim(trim((string) $hex), '#');
if (strlen($hex) === 3) {
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
}
if (! preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
return null;
}
return [hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2))];
};
$luminance = static function (array $rgb): float {
$linear = array_map(static function ($c) {
$c /= 255;
return $c <= 0.03928 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
}, $rgb);
return 0.2126 * $linear[0] + 0.7152 * $linear[1] + 0.0722 * $linear[2];
};
$baseRgb = $parseHex($base);
$againstRgb = $parseHex($against);
$computed = null;
if ($ratio !== null && $ratio !== '') {
$computed = (float) $ratio;
} elseif ($baseRgb && $againstRgb) {
$l1 = $luminance($baseRgb);
$l2 = $luminance($againstRgb);
$computed = (max($l1, $l2) + 0.05) / (min($l1, $l2) + 0.05);
}
// Resolve the grade: explicit override wins, otherwise derive from the ratio.
$grade = $passes;
if (is_string($grade)) {
$grade = strtolower($grade) === 'fail' ? 'fail' : strtoupper($grade);
}
if (($grade === null || $grade === '') && $computed !== null) {
$grade = $computed >= 7 ? 'AAA' : ($computed >= 4.5 ? 'AA' : 'fail');
}
$isFail = $grade === 'fail' || $grade === false;
$isPass = ! $isFail && $grade !== null && $grade !== '';
$gradeLabel = match (true) {
$grade === 'AAA' => 'AAA',
$grade === 'AA' => 'AA',
$grade === true => __('Pass'),
$isFail => __('Fail'),
default => '—',
};
$ratioText = $computed !== null ? number_format($computed, 2).':1' : '—';
$variant = in_array($variant, ['pill', 'icon'], true) ? $variant : 'pill';
@endphp
<span
data-slot="color-contrast"
role="img"
aria-label="{{ __('Contrast :ratio :grade', ['ratio' => $ratioText, 'grade' => $isPass ? $gradeLabel : __('Fail')]) }}"
{{ $attributes->merge(['class' => 'inline-flex items-center gap-2 rounded-md border border-border bg-card px-2 py-1 align-middle']) }}
>
@if ($baseRgb && $againstRgb)
{{-- Swatch colours are arbitrary user data (any CSS colour), never palette utilities. --}}
<span
data-slot="color-contrast-swatch"
class="inline-flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-md border border-border"
aria-hidden="true"
style="background-color: {{ $against }}"
>
<span class="font-mono text-xs font-semibold leading-none" style="color: {{ $base }}">Aa</span>
</span>
@endif
@if ($label)
<span class="text-xs font-medium text-foreground">{{ $label }}</span>
@endif
<span data-slot="color-contrast-ratio" class="font-mono text-xs text-muted-foreground">{{ $ratioText }}</span>
@if ($variant === 'icon')
<span
data-slot="color-contrast-badge"
@class([
'inline-flex size-4 shrink-0 items-center justify-center rounded-full',
'bg-success/15 text-success' => $isPass,
'bg-destructive/15 text-destructive' => $isFail,
'bg-muted text-muted-foreground' => ! $isPass && ! $isFail,
])
>
@if ($isPass)
<svg xmlns="http://www.w3.org/2000/svg" class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5" /></svg>
@elseif ($isFail)
<svg xmlns="http://www.w3.org/2000/svg" class="size-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>
@else
<span class="text-2xs font-semibold">?</span>
@endif
</span>
@else
<span
data-slot="color-contrast-badge"
@class([
'inline-flex items-center rounded-full px-2 py-0.5 text-2xs font-semibold uppercase tracking-wide',
'bg-success/15 text-success' => $isPass,
'bg-destructive/15 text-destructive' => $isFail,
'bg-muted text-muted-foreground' => ! $isPass && ! $isFail,
])
>{{ $gradeLabel }}</span>
@endif
</span>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.4 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0