Theming Native Controls with accent-color
Introduction
Native checkboxes, radio buttons, range sliders and progress bars arrive in the operating system's default blue (or
grey). For years, the only way to match them to a brand colour was to replace them entirely: hide the real input,
build a styled <div> in its place, and then re-implement keyboard handling, focus states and accessibility
semantics by hand.
accent-color themes the native controls directly. One property, real <input> elements, and all of their
native behaviour kept.
The Property
input[type="checkbox"],
input[type="radio"],
input[type="range"],
progress {
accent-color: #22c55e;
}accent-color tells the browser which colour to use for the "on" part of a control: the fill behind a checkmark,
the selected radio dot, the filled part of a range track, the completed part of a progress bar. The browser still
draws its own native shape and keeps its own interaction; only the accent changes.
It takes a single value: any CSS colour, or auto — the initial value, which leaves the choice to the browser.
Which Controls It Themes
| Control | What takes the accent colour |
|---|---|
<input type="checkbox"> | The box fill when checked or indeterminate |
<input type="radio"> | The selected dot and ring |
<input type="checkbox" switch> | The "on" track, in browsers that draw a native switch |
<input type="range"> | The filled part of the track and the thumb |
<progress> | The completed bar, including the indeterminate bar |
The specification names checkboxes, radio buttons, range sliders and progress bars. The switch is still a checkbox underneath, so it follows the same rule wherever a browser renders it as a switch.
Set It Once, Inherit Everywhere
:root {
accent-color: var(--brand);
}accent-color inherits, so setting it once on :root (or on any container) themes every native control inside it.
There's no need to target input[type="checkbox"], input[type="radio"] and the rest one by one.
When one area should stand apart, override it there — the controls inside pick up the nearer value:
:root {
accent-color: var(--brand);
}
/* Destructive settings get a warning colour instead of the brand */
.danger-zone {
accent-color: crimson;
}Because it's an ordinary colour value, it combines with everything else you already use for themes — a custom
property, or light-dark() for a separate light and dark accent:
:root {
color-scheme: light dark;
accent-color: light-dark(#15803d, #4ade80);
}Contrast Is Handled for You
You only choose the accent. The browser chooses what sits on top of it: on a dark accent the checkmark is drawn light, and on a pale accent it's drawn dark, so the checked state stays readable whichever colour you pick. Chrome treats range sliders and progress bars the same way, drawing the unfilled part of the track light behind a dark accent and dark behind a pale one — so a pale accent can give you a dark track even on a light page.
Try It Live
The sandbox below puts every control accent-color themes side by side: the browser default in one column, the
same markup inside a container with one accent-color declaration in the other. Pick a colour and watch the right
column follow, while the left keeps the browser's own accent.
- The checkbox, radio button, range slider and progress rows show the four controls the specification lists.
- The indeterminate checkbox and progress rows show that the "in between" states take the accent too.
- The switch row tells you whether your browser draws a native switch or a plain checkbox.
- The Left alone panel shows a control the property deliberately doesn't reach.
Theming Native Controls with accent-color
Every native control accent-color themes — checkbox, indeterminate checkbox, radio, switch, range slider, and determinate and indeterminate progress — shown beside its browser default, re-themed live from one colour picker, plus a <meter> to show what the property leaves alone.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>accent-color Playground</title> </head> <body> <main class="playground"> <header class="playground-header"> <h1>Theming Native Controls with accent-color</h1> <p>Every control here is the browser's own element. The left column is the browser default; the right column is the same markup inside a container with one <code>accent-color</code> declaration.</p> </header> <fieldset class="control-group"> <legend>Theme</legend> <label class="picker-row" for="theme-picker"> Accent colour <input type="color" id="theme-picker" value="#22c55e" /> </label> </fieldset> <section class="demo-area" aria-label="Native controls, default and themed"> <div class="compare-grid"> <span class="column-heading" aria-hidden="true">Control</span> <span class="column-heading" id="col-default" aria-hidden="true">Browser default</span> <span class="column-heading" id="col-themed" aria-hidden="true">accent-color</span> <span class="row-label" id="label-checkbox">Checkbox</span> <span class="cell default"><input type="checkbox" checked aria-labelledby="label-checkbox col-default" /></span> <span class="cell themed"><input type="checkbox" checked aria-labelledby="label-checkbox col-themed" /></span> <span class="row-label" id="label-mixed">Checkbox, indeterminate</span> <span class="cell default"><input type="checkbox" data-indeterminate aria-labelledby="label-mixed col-default" /></span> <span class="cell themed"><input type="checkbox" data-indeterminate aria-labelledby="label-mixed col-themed" /></span> <span class="row-label" id="label-radio">Radio button</span> <span class="cell default"><input type="radio" name="default-plan" checked aria-labelledby="label-radio col-default" /></span> <span class="cell themed"><input type="radio" name="themed-plan" checked aria-labelledby="label-radio col-themed" /></span> <span class="row-label" id="label-switch">Switch <small id="switch-support"></small></span> <span class="cell default"><input type="checkbox" switch checked aria-labelledby="label-switch col-default" /></span> <span class="cell themed"><input type="checkbox" switch checked aria-labelledby="label-switch col-themed" /></span> <span class="row-label" id="label-range">Range slider</span> <span class="cell default"><input type="range" min="0" max="100" value="65" aria-labelledby="label-range col-default" /></span> <span class="cell themed"><input type="range" min="0" max="100" value="65" aria-labelledby="label-range col-themed" /></span> <span class="row-label" id="label-progress">Progress</span> <span class="cell default"><progress value="70" max="100" aria-labelledby="label-progress col-default"></progress></span> <span class="cell themed"><progress value="70" max="100" aria-labelledby="label-progress col-themed"></progress></span> <span class="row-label" id="label-busy">Progress, indeterminate</span> <span class="cell default"><progress aria-labelledby="label-busy col-default"></progress></span> <span class="cell themed"><progress aria-labelledby="label-busy col-themed"></progress></span> </div> </section> <fieldset class="control-group"> <legend>Left alone</legend> <div class="compare-grid"> <span class="row-label" id="label-meter">Meter</span> <span class="cell default"><meter value="0.7" aria-labelledby="label-meter col-default"></meter></span> <span class="cell themed"><meter value="0.7" aria-labelledby="label-meter col-themed"></meter></span> </div> <p class="tip"><code><meter></code> keeps its own good/average/poor colours, since those carry meaning, so <code>accent-color</code> doesn't reach it.</p> </fieldset> <output class="readout" id="readout" aria-live="polite"></output> </main> <script src="./index.js"></script> </body> </html>
Starting sandbox…
No console output yet.
No original version of /index.html to compare against.
The In-Between States
Both "busy" states pick up the accent with no extra CSS.
<input type="checkbox" id="select-all" />
<!-- No value attribute: an animated, indeterminate bar -->
<progress aria-label="Uploading"></progress>// There's no HTML attribute for this state; it can only be set from script.
document.getElementById("select-all").indeterminate = true;An indeterminate checkbox — the classic "some items selected" state of a select all box — fills with the accent
and shows a dash instead of a tick. A <progress> element with no value becomes an indeterminate, animated bar,
and the moving segment takes the accent as well.
The Native Switch
<label>
<input type="checkbox" switch checked />
Email notifications
</label>Adding the switch attribute to a checkbox asks the browser to draw it as an on/off switch, and browsers that draw
one also colour its "on" track with accent-color. Browsers that don't recognise the attribute simply render an
ordinary checkbox — still fully working and still accent-coloured — so it's safe to use today. The sandbox's switch
row reports which of the two your browser is showing.
What It Doesn't Change
accent-color only affects the colour of the checked or filled part. It doesn't change:
- The control's size or shape, which is still whatever the browser and operating system draw
- The unchecked checkbox and empty radio ring, which follow
color-schemeinstead - Label text, since
accent-colorhas no effect on a<label>'s colour - Fonts — form controls don't inherit your typography by default either; the
modern CSS reset fixes that with
font: inherit <meter>, which keeps its own good, average and poor colours because they carry meaning<select>menus, date pickers and colour pickers, which are outside its scope
For anything deeper — a completely custom checkbox shape, or a track with a gradient — you're back to either
accepting the native look with just the accent themed, or building a full custom replacement. accent-color covers
the middle ground: keep it native, just match the brand.
Where Should You Use This?
- Forms where checkboxes and radio buttons need to match the brand, without rebuilding them as custom controls
- Settings screens full of switches, sliders and progress indicators
- Anywhere a custom-styled checkbox or radio replacement exists purely for colour, where native behaviour would be the better choice
- Quick brand-matching for admin dashboards and internal tools that don't justify a full design system
Production Considerations & Edge Cases
- Contrast still matters. The browser keeps the checkmark readable, but the accent itself has to stand out from what's around it. WCAG asks for a contrast ratio of at least 3:1 between a control's state and its adjacent colours (success criterion 1.4.11, non-text contrast), so check your accent against the page background.
- Forced colours mode overrides it. When someone uses a forced colours mode such as Windows Contrast Themes, the
browser resets
accent-colortoautoand draws controls in their chosen system colours. That's the right outcome — never rely on the accent alone to carry meaning. - It really is the native control. Screen readers, keyboard navigation and autofill all behave exactly as they do for an unstyled input, because nothing about the element's semantics or behaviour has changed.
- Rendering differs a little between browsers and operating systems.
accent-colortells each browser what colour to use, but the exact shape and animation of the checked state are still its own. That's a strength (native, familiar, accessible) as much as a limit (you can't match a design comp pixel for pixel). - Browser support is solid. Every current evergreen browser supports it (Chrome and Edge 93+, Firefox 92+, Safari 15.4+), so it's safe to adopt broadly. Older browsers just show their default accent.
Key Takeaway
accent-color is the answer to "I just need this checkbox to match the brand". Reach for it before reaching for a
custom control, and save the heavier rebuild for designs that genuinely need a shape or interaction native controls
can't provide.
Changelog
- — Initial publication.