Your Quasar color palette is eight brand colors: primary, secondary, accent, dark, positive, negative, info and warning. Every q-btn, q-badge, q-chip and banner draws from them, and Quasar doesn't check whether text on them is readable. Its accessibility docs say so directly: there's no contrast enforcement, in light or dark mode. Today we're launching the free Brand Color Contrast Checker to do that check for all eight at once. Below is what it measures, why the stock Quasar colors already fail some of it, and how to fix a failing color without losing your brand.
WCAG 2.2 has three contrast rules that matter for brand colors:
| Rule | Level | Minimum ratio |
|---|---|---|
| 1.4.3 Contrast (Minimum): normal text | AA | 4.5:1 |
| 1.4.3: large text (at least 18pt, or 14pt bold) | AA | 3:1 |
| 1.4.11 Non-text Contrast: UI components and states | AA | 3:1 |
| 1.4.6 Contrast (Enhanced): normal / large text | AAA | 7:1 / 4.5:1 |
18pt is 24px and 14pt is about 18.7px, so most button labels count as normal text and need 4.5:1. Text in a disabled control is exempt.
The ratio is (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter color and L2 of the darker one. Relative luminance weights the linearized red, green and blue channels by how bright they look: green counts ten times as much as blue. That's why a pale yellow and a deep blue at the "same" HSL lightness give completely different results. The post on OKLCH color scales goes into why lightness is so hard to judge by eye.
Here are Quasar 2.35.2's default brand colors (from quasar/src/css/variables.sass), measured two ways: white text on the color, which is what <q-btn color="..."> renders, and the color used as text on the default dark page ($dark-page, #121212):
| Color | Hex | White text on it | As text on #121212 |
|---|---|---|---|
| primary | #1976D2 | 4.60 | 4.07 |
| secondary | #26A69A | 2.99 | 6.24 |
| accent | #9C27B0 | 6.30 | 2.97 |
| positive | #21BA45 | 2.56 | 7.29 |
| negative | #C10015 | 6.40 | 2.92 |
| info | #31CCEC | 1.91 | 9.79 |
| warning | #F2C037 | 1.69 | 11.03 |
Read it against 4.5:1:
<q-btn color="positive" label="Save" /> is 2.56:1. color="secondary" misses even the 3:1 large-text bar, by a hair.warning type sets textColor: 'dark'. A plain q-btn color="warning" doesn't: it renders white text, at 1.69:1.None of this is a bug in Quasar. A palette can't be right for white text and for dark backgrounds at once. That's the point: you have to choose per use, per mode.
A color that works in one theme often fails in the other, and most teams only test the theme they design in. The default primary is the cleanest example: white text on #1976D2 passes at 4.60, but the same blue as a link or a text-primary label on the dark page is 4.07. In dark mode you want a lighter primary, and once it's lighter, white text on it fails, so filled buttons need dark text in dark mode.
That's why the Theme Builder treats light and dark as separate stops of one scale. By default a brand color uses shade 500 in light mode and 400 in dark mode, and text on a filled brand color uses an "inverted" token (white in light mode, neutral-900 in dark mode).
It's also a reminder to check your own defaults. With the Theme Builder's Default preset, the green primary (Tailwind green-500, #00C950) carries white text at 2.21:1 in light mode. In dark mode, green-400 with neutral-900 text is 10.02:1. If you keep that green and need AA in light mode, move the light-mode primary to shade 700 (4.94:1), as shown below.
The Brand Color Contrast Checker is built for exactly this table, for your colors instead of the defaults:
$dark-page, or paste a Theme Builder share link or your quasar.variables.scss to load them.text-color="dark"), and the color as text on the dark page. Pick AA, AA large or AAA.quasar.variables.scss values, plus a body.body--dark block for the dark-mode colors.It runs in your browser, without signing in. Ratios are cut, not rounded, to two decimals, so a 4.499 never shows as 4.50. The table above uses the same convention.
If you want the same check in a unit test (handy when a designer changes quasar.variables.scss), the math is short:
function channel(hex: string, start: number) {
const value = Number.parseInt(hex.slice(start, start + 2), 16) / 255
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4
}
export function relativeLuminance(hex: string) {
return 0.2126 * channel(hex, 1) + 0.7152 * channel(hex, 3) + 0.0722 * channel(hex, 5)
}
export function contrastRatio(a: string, b: string) {
const [light, dark] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x)
return (light! + 0.05) / (dark! + 0.05)
}
const brand = {
primary: '#1976D2',
secondary: '#26A69A',
accent: '#9C27B0',
positive: '#21BA45',
negative: '#C10015',
info: '#31CCEC',
warning: '#F2C037',
}
for (const [name, color] of Object.entries(brand)) {
const whiteText = contrastRatio(color, '#FFFFFF')
const onDarkPage = contrastRatio(color, '#121212')
console.log(name.padEnd(10), whiteText.toFixed(2), onDarkPage.toFixed(2))
}It takes six-digit hex colors, which is what a quasar.variables.scss file holds. Quasar also ships colors.luminosity() (import { colors } from quasar), which computes the same relative luminance from a hex or rgb() string. It doesn't parse oklch(), so for an OKLCH theme, run the check on the hex values or use the tool.
You rarely need a new brand color. One of these usually does it:
<q-btn color="warning" text-color="dark" />, or text-color="black".body.body--dark, for example a lighter primary for text and links. Then check filled buttons again with dark-mode text.Whatever you change, check both modes again. A fix for one theme is the most common way to break the other.
Does Quasar check contrast for me? No. The accessibility docs list color contrast as your app's responsibility, in light and dark mode.
Do icons need 4.5:1? No. Icons that carry meaning, focus rings, input borders and similar UI parts need 3:1 against what's next to them (WCAG 1.4.11). Decorative icons have no requirement.
Is AA enough? AA (4.5:1 for normal text) is the level most audits and accessibility policies target. AAA (7:1) is stricter and hard to meet with saturated brand fills.
What about 4.49:1? It fails. WCAG gives a minimum and no rounding rule, so treat anything under 4.5 as a fail, which is why the table above shows secondary as 2.99 rather than 3.00.
Paste your eight colors into the Brand Color Contrast Checker, or a Theme Builder share link, and see which pairs fail in light and dark mode before your users do.