CSS 41 💻 prefers-color scheme and prefers-contrast media features
These two media features are essential for accessibility and personalization. They let you respect the user’s preferred color scheme (light/dark) and contrast level.
Overview of Features
| Feature | Description | Values |
|---|---|---|
prefers-color-scheme | User’s preferred color scheme | light, dark |
prefers-contrast | User’s preferred contrast level | no-preference, more, less, custom |
1. prefers-color-scheme
The prefers-color-scheme media feature detects whether the user prefers a light or dark color theme.
@media (prefers-color-scheme: dark) {
body {
background-color: #333;
color: white;
}
}
Values
| Value | Description |
|---|---|
light | User prefers a light theme (or has no preference) |
dark | User prefers a dark theme |
How It Works
- The user sets their preference in their operating system settings
- Windows: Settings → Personalization → Colors → Choose your mode
- macOS: System Preferences → General → Appearance
- iOS: Settings → Display & Brightness → Appearance
- Android: Settings → Display → Dark theme
Key Points:
lightis the default if no preference is expressed- Always provide a light theme as the base, then enhance for dark
- Use the same content and structure — only change colors
- Test both themes for contrast and readability
2. prefers-contrast
The prefers-contrast media feature detects whether the user prefers more or less contrast.
@media (prefers-contrast: more) {
body {
background-color: #f0f0f0;
color: #1a1a1a;
}
}
Values
| Value | Description |
|---|---|
no-preference | No specific preference (default) |
more | User prefers higher contrast |
less | User prefers lower contrast |
custom | User uses a specific set of colors/contrast |
How It Works
- The user sets their preference in their operating system accessibility settings
- Windows: Settings → Ease of Access → Display → High contrast
- macOS: System Preferences → Accessibility → Display → Increase contrast
- iOS: Settings → Accessibility → Display & Text Size → Increase Contrast
Key Points:
more— increase contrast for better legibilityless— reduce contrast (for users sensitive to bright colors)custom— user has chosen a specific color scheme (often withforced-colors)- Always ensure WCAG AAA contrast (7:1) for
more
Complete Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>prefers-color-scheme and prefers-contrast</title>
<style>
*, *::before, *::after {
box-sizing: border-box;
}
/* ====== BASE (LIGHT THEME) ====== */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #212529;
--text-secondary: #6c757d;
--accent: #007bff;
--accent-hover: #0056b3;
--border: #dee2e6;
--shadow: rgba(0, 0, 0, 0.1);
--code-bg: #1e1e1e;
--code-text: #d4d4d4;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 20px;
background: var(--bg-secondary);
color: var(--text-primary);
max-width: 1200px;
margin: 0 auto;
line-height: 1.6;
transition: background 0.3s, color 0.3s;
}
h1 {
color: var(--accent);
border-bottom: 3px solid var(--accent);
padding-bottom: 10px;
}
h2 {
color: var(--accent);
border-left: 4px solid var(--accent);
padding-left: 15px;
margin-top: 30px;
}
h3 {
color: var(--text-primary);
margin-top: 20px;
}
section {
background: var(--bg-primary);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow);
margin: 20px 0;
transition: all 0.3s;
}
.code-block {
background: var(--code-bg);
color: var(--code-text);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.8;
margin: 10px 0;
}
.reference-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.reference-table th,
.reference-table td {
padding: 10px;
border: 1px solid var(--border);
text-align: left;
}
.reference-table th {
background: var(--accent);
color: white;
}
.reference-table tr:nth-child(even) {
background: var(--bg-tertiary);
}
.note {
font-size: 0.9rem;
color: var(--text-secondary);
margin-top: 5px;
}
.highlight {
background: #ffc107;
color: #333;
padding: 2px 6px;
border-radius: 4px;
font-weight: bold;
}
/* ====== DARK THEME ====== */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #0d0d0d;
--bg-tertiary: #2d2d2d;
--text-primary: #f8f9fa;
--text-secondary: #adb5bd;
--accent: #4dabf7;
--accent-hover: #339af0;
--border: #444;
--shadow: rgba(0, 0, 0, 0.5);
--code-bg: #0d0d0d;
--code-text: #d4d4d4;
}
.highlight {
background: #4dabf7;
color: #000;
}
}
/* ====== HIGH CONTRAST ====== */
@media (prefers-contrast: more) {
:root {
--bg-primary: #ffffff;
--bg-secondary: #ffffff;
--bg-tertiary: #f0f0f0;
--text-primary: #000000;
--text-secondary: #1a1a1a;
--accent: #0000cc;
--accent-hover: #000099;
--border: #000000;
--shadow: rgba(0, 0, 0, 0.3);
}
body {
border: 2px solid #000;
padding: 25px;
}
section {
border: 2px solid #000;
box-shadow: none;
}
h1 {
border-bottom-width: 4px;
}
h2 {
border-left-width: 6px;
}
.code-block {
background: #000;
color: #fff;
border: 2px solid #000;
}
.reference-table th {
background: #000;
color: #fff;
}
.reference-table td {
border-color: #000;
}
.note {
color: #1a1a1a;
}
.highlight {
background: #000;
color: #fff;
border: 1px solid #000;
}
}
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
:root {
--bg-primary: #000000;
--bg-secondary: #000000;
--bg-tertiary: #1a1a1a;
--text-primary: #ffffff;
--text-secondary: #f0f0f0;
--accent: #66b3ff;
--accent-hover: #99ccff;
--border: #ffffff;
}
body {
border-color: #fff;
}
section {
border-color: #fff;
}
.code-block {
background: #000;
color: #fff;
border-color: #fff;
}
.reference-table th {
background: #fff;
color: #000;
}
.reference-table td {
border-color: #fff;
}
.highlight {
background: #fff;
color: #000;
}
}
/* ====== LOW CONTRAST ====== */
@media (prefers-contrast: less) {
:root {
--bg-primary: #2d2d2d;
--bg-secondary: #1a1a1a;
--bg-tertiary: #3d3d3d;
--text-primary: #d0d0d0;
--text-secondary: #999;
--accent: #6c9bd2;
--accent-hover: #8aadd4;
--border: #444;
--shadow: rgba(0, 0, 0, 0.3);
}
.highlight {
background: #6c9bd2;
color: #1a1a1a;
}
}
/* ====== DETECTION CARDS ====== */
.detection-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.detection-card {
padding: 15px;
border-radius: 8px;
text-align: center;
border: 2px solid var(--border);
background: var(--bg-tertiary);
transition: all 0.3s;
}
.detection-card .label {
font-size: 0.85rem;
color: var(--text-secondary);
display: block;
margin-bottom: 5px;
}
.detection-card .value {
font-size: 1.1rem;
font-weight: bold;
color: var(--accent);
}
.detection-card.active {
border-color: var(--accent);
background: var(--bg-primary);
}
/* ====== LIVE INDICATOR ====== */
.live-indicator {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--accent);
color: white;
padding: 12px 20px;
border-radius: 50px;
font-weight: bold;
font-size: 0.85rem;
box-shadow: 0 4px 15px var(--shadow);
z-index: 1000;
transition: all 0.3s;
display: flex;
flex-direction: column;
gap: 3px;
}
.live-indicator .detail {
font-size: 0.75rem;
opacity: 0.9;
}
/* ====== PRACTICAL: THEME TOGGLE DEMO ====== */
.theme-demo {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 15px 0;
}
.theme-card {
flex: 1;
min-width: 250px;
padding: 20px;
border-radius: 12px;
border: 2px solid var(--border);
background: var(--bg-primary);
transition: all 0.3s;
}
.theme-card h4 {
margin-top: 0;
color: var(--accent);
}
.theme-card .preview {
padding: 15px;
border-radius: 8px;
background: var(--bg-tertiary);
margin: 10px 0;
text-align: center;
font-weight: bold;
border: 1px solid var(--border);
}
/* Light preview */
.light-preview {
background: #ffffff;
color: #212529;
}
/* Dark preview */
.dark-preview {
background: #1a1a1a;
color: #f8f9fa;
}
/* ====== CONTRAST VISUALIZATION ====== */
.contrast-demo {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 15px 0;
}
.contrast-box {
flex: 1;
min-width: 200px;
padding: 20px;
border-radius: 8px;
text-align: center;
font-weight: bold;
border: 2px solid var(--border);
}
.contrast-normal {
background: #ffffff;
color: #6c757d;
}
.contrast-more {
background: #ffffff;
color: #000000;
border-color: #000;
}
.contrast-less {
background: #2d2d2d;
color: #999;
}
/* ====== ACCESSIBILITY TIPS ====== */
.tip-box {
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid var(--accent);
background: var(--bg-tertiary);
}
.tip-box.success {
border-left-color: #28a745;
}
.tip-box.warning {
border-left-color: #ffc107;
}
.tip-box.danger {
border-left-color: #dc3545;
}
</style>
</head>
<body>
<h1>prefers-color-scheme and prefers-contrast</h1>
<!-- ====== 1. COLOR SCHEME DEMO ====== -->
<section>
<h2>1. prefers-color-scheme</h2>
<p>Detects the user's preferred <strong>color scheme</strong> (light or dark).</p>
<div class="theme-demo">
<div class="theme-card">
<h4>☀️ Light Theme</h4>
<div class="preview light-preview">
This is a light theme preview
</div>
<p class="note">Applied when user prefers light mode (or has no preference).</p>
</div>
<div class="theme-card">
<h4>🌙 Dark Theme</h4>
<div class="preview dark-preview">
This is a dark theme preview
</div>
<p class="note">Applied when user prefers dark mode.</p>
</div>
</div>
<div class="code-block">
/* Light theme (default) */
:root {
--bg-primary: #ffffff;
--text-primary: #212529;
--accent: #007bff;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1a1a1a;
--text-primary: #f8f9fa;
--accent: #4dabf7;
}
}
</div>
<p class="note"><strong>Try it:</strong> Change your OS theme to dark mode and watch this page adapt!</p>
</section>
<!-- ====== 2. CONTRAST DEMO ====== -->
<section>
<h2>2. prefers-contrast</h2>
<p>Detects the user's preferred <strong>contrast level</strong>.</p>
<div class="contrast-demo">
<div class="contrast-box contrast-normal">
Normal Contrast<br>
<small>#6c757d on #ffffff</small>
</div>
<div class="contrast-box contrast-more">
More Contrast<br>
<small>#000000 on #ffffff</small>
</div>
<div class="contrast-box contrast-less">
Less Contrast<br>
<small>#999 on #2d2d2d</small>
</div>
</div>
<div class="code-block">
/* High contrast preference */
@media (prefers-contrast: more) {
:root {
--text-primary: #000000;
--bg-primary: #ffffff;
--accent: #0000cc;
}
section {
border: 2px solid #000;
box-shadow: none;
}
}
/* Low contrast preference */
@media (prefers-contrast: less) {
:root {
--bg-primary: #2d2d2d;
--text-primary: #d0d0d0;
--accent: #6c9bd2;
}
}
</div>
</section>
<!-- ====== 3. COMBINED THEME + CONTRAST ====== -->
<section>
<h2>3. Combined Color Scheme + Contrast</h2>
<p>Both features can work together for a fully personalized experience.</p>
<div class="code-block">
/* Dark + High Contrast */
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
:root {
--bg-primary: #000000;
--text-primary: #ffffff;
--accent: #66b3ff;
--border: #ffffff;
}
}
/* Dark + Low Contrast */
@media (prefers-contrast: less) and (prefers-color-scheme: dark) {
:root {
--bg-primary: #2d2d2d;
--text-primary: #d0d0d0;
}
}
</div>
</section>
<!-- ====== 4. DETECTION SUMMARY ====== -->
<section>
<h2>4. Your Preferences</h2>
<p>Below is a live detection of your preferences.</p>
<div class="detection-grid">
<div class="detection-card" id="scheme-light">
<span class="label">prefers-color-scheme: light</span>
<span class="value" id="scheme-light-value">Detecting...</span>
</div>
<div class="detection-card" id="scheme-dark">
<span class="label">prefers-color-scheme: dark</span>
<span class="value" id="scheme-dark-value">Detecting...</span>
</div>
<div class="detection-card" id="contrast-no-pref">
<span class="label">prefers-contrast: no-preference</span>
<span class="value" id="contrast-no-pref-value">Detecting...</span>
</div>
<div class="detection-card" id="contrast-more">
<span class="label">prefers-contrast: more</span>
<span class="value" id="contrast-more-value">Detecting...</span>
</div>
<div class="detection-card" id="contrast-less">
<span class="label">prefers-contrast: less</span>
<span class="value" id="contrast-less-value">Detecting...</span>
</div>
<div class="detection-card" id="contrast-custom">
<span class="label">prefers-contrast: custom</span>
<span class="value" id="contrast-custom-value">Detecting...</span>
</div>
</div>
<div class="code-block">
/* Detect color scheme */
@media (prefers-color-scheme: light) { /* Light preference */ }
@media (prefers-color-scheme: dark) { /* Dark preference */ }
/* Detect contrast preference */
@media (prefers-contrast: no-preference) { /* Default */ }
@media (prefers-contrast: more) { /* High contrast */ }
@media (prefers-contrast: less) { /* Low contrast */ }
@media (prefers-contrast: custom) { /* Custom */ }
</div>
</section>
<!-- ====== 5. ACCESSIBILITY TIPS ====== -->
<section>
<h2>5. Accessibility Tips</h2>
<div class="tip-box success">
<strong>✅ Do This:</strong>
<ul style="margin: 5px 0;">
<li>Start with light theme as the base</li>
<li>Use CSS custom properties (variables) for easy theming</li>
<li>Test both light and dark themes for contrast</li>
<li>Respect the user's system preferences</li>
<li>Ensure WCAG AA (4.5:1) or AAA (7:1) contrast</li>
</ul>
</div>
<div class="tip-box warning">
<strong>⚠️ Be Careful:</strong>
<ul style="margin: 5px 0;">
<li>Don't use pure black (#000) on pure white (#fff) — it's harsh</li>
<li>Don't use pure white (#fff) on pure black (#000) — it causes halation</li>
<li>Don't forget to test with actual users</li>
<li>Don't override user preferences without good reason</li>
</ul>
</div>
<div class="tip-box danger">
<strong>❌ Don't Do This:</strong>
<ul style="margin: 5px 0;">
<li>Don't ignore user preferences — it's an accessibility issue</li>
<li>Don't use low-contrast text (#999 on #fff)</li>
<li>Don't rely solely on color to convey information</li>
<li>Don't forget about images — they need dark mode variants too</li>
</ul>
</div>
</section>
<!-- ====== 6. REFERENCE TABLES ====== -->
<section>
<h2>6. Reference Tables</h2>
<h3>prefers-color-scheme Values</h3>
<table class="reference-table">
<tr>
<th>Value</th>
<th>Description</th>
</tr>
<tr>
<td><code>light</code></td>
<td>User prefers a light theme (or has no preference)</td>
</tr>
<tr>
<td><code>dark</code></td>
<td>User prefers a dark theme</td>
</tr>
</table>
<h3>prefers-contrast Values</h3>
<table class="reference-table">
<tr>
<th>Value</th>
<th>Description</th>
</tr>
<tr>
<td><code>no-preference</code></td>
<td>No specific preference (default)</td>
</tr>
<tr>
<td><code>more</code></td>
<td>User prefers higher contrast</td>
</tr>
<tr>
<td><code>less</code></td>
<td>User prefers lower contrast</td>
</tr>
<tr>
<td><code>custom</code></td>
<td>User uses a specific set of colors/contrast</td>
</tr>
</table>
<h3>WCAG Contrast Requirements</h3>
<table class="reference-table">
<tr>
<th>Level</th>
<th>Normal Text</th>
<th>Large Text</th>
</tr>
<tr>
<td><strong>AA</strong></td>
<td>4.5:1</td>
<td>3:1</td>
</tr>
<tr>
<td><strong>AAA</strong></td>
<td>7:1</td>
<td>4.5:1</td>
</tr>
</table>
</section>
<!-- ====== 7. BEST PRACTICES ====== -->
<section>
<h2>7. Best Practices</h2>
<div style="background: #d4edda; padding: 20px; border-radius: 8px; border-left: 4px solid #28a745;">
<h3 style="margin-top: 0; color: #155724;">✅ Do This:</h3>
<ul>
<li>Use CSS custom properties for theming</li>
<li>Start with light theme, then add dark theme overrides</li>
<li>Test both themes for readability and contrast</li>
<li>Respect <code>prefers-contrast: more</code> for accessibility</li>
<li>Use <code>prefers-contrast: less</code> for users sensitive to bright colors</li>
<li>Combine both features for a fully personalized experience</li>
<li>Test with real users who have accessibility needs</li>
</ul>
</div>
<div style="background: #f8d7da; padding: 20px; border-radius: 8px; border-left: 4px solid #dc3545; margin-top: 15px;">
<h3 style="margin-top: 0; color: #721c24;">❌ Don't Do This:</h3>
<ul>
<li>Don't ignore user preferences — it's an accessibility issue</li>
<li>Don't use pure black on pure white (or vice versa)</li>
<li>Don't forget to test with actual users</li>
<li>Don't use color alone to convey information</li>
<li>Don't force a theme on users without an option to change</li>
</ul>
</div>
</section>
<!-- ====== LIVE INDICATOR ====== -->
<div class="live-indicator" id="live-indicator">
<span id="indicator-scheme">Scheme: —</span>
<span class="detail" id="indicator-contrast">Contrast: —</span>
</div>
<!-- ====== SCRIPT FOR DETECTION ====== -->
<script>
function updateIndicators() {
// Color scheme detection
const schemeLight = document.getElementById('scheme-light');
const schemeDark = document.getElementById('scheme-dark');
const isLight = window.matchMedia('(prefers-color-scheme: light)').matches;
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
function setCard(card, isActive, activeText, inactiveText) {
const value = card.querySelector('.value');
if (isActive) {
value.textContent = activeText;
card.classList.add('active');
} else {
value.textContent = inactiveText;
card.classList.remove('active');
}
}
setCard(schemeLight, isLight, 'Preferred ✅', 'Not preferred');
setCard(schemeDark, isDark, 'Preferred ✅', 'Not preferred');
// Contrast detection
const contrastNoPref = document.getElementById('contrast-no-pref');
const contrastMore = document.getElementById('contrast-more');
const contrastLess = document.getElementById('contrast-less');
const contrastCustom = document.getElementById('contrast-custom');
const isNoPref = window.matchMedia('(prefers-contrast: no-preference)').matches;
const isMore = window.matchMedia('(prefers-contrast: more)').matches;
const isLess = window.matchMedia('(prefers-contrast: less)').matches;
const isCustom = window.matchMedia('(prefers-contrast: custom)').matches;
setCard(contrastNoPref, isNoPref, 'Active ✅', 'Not active');
setCard(contrastMore, isMore, 'Active ✅', 'Not active');
setCard(contrastLess, isLess, 'Active ✅', 'Not active');
setCard(contrastCustom, isCustom, 'Active ✅', 'Not active');
// Live indicator
const indicatorScheme = document.getElementById('indicator-scheme');
const indicatorContrast = document.getElementById('indicator-contrast');
const indicator = document.getElementById('live-indicator');
let schemeText = isDark ? '🌙 Dark' : '☀️ Light';
indicatorScheme.textContent = 'Scheme: ' + schemeText;
let contrastText = 'Default';
if (isMore) contrastText = 'More';
else if (isLess) contrastText = 'Less';
else if (isCustom) contrastText = 'Custom';
indicatorContrast.textContent = 'Contrast: ' + contrastText;
// Update indicator color based on contrast
if (isMore) {
indicator.style.background = '#000';
indicator.style.color = '#fff';
indicator.style.border = '2px solid #fff';
} else if (isLess) {
indicator.style.background = '#6c9bd2';
indicator.style.color = '#1a1a1a';
indicator.style.border = 'none';
} else {
indicator.style.background = isDark ? '#4dabf7' : '#007bff';
indicator.style.color = 'white';
indicator.style.border = 'none';
}
}
window.addEventListener('load', updateIndicators);
// Listen for changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateIndicators);
window.matchMedia('(prefers-contrast: more)').addEventListener('change', updateIndicators);
window.matchMedia('(prefers-contrast: less)').addEventListener('change', updateIndicators);
updateIndicators();
</script>
</body>
</html>
Quick Reference
| Feature | Description | Values |
|---|---|---|
prefers-color-scheme | User’s preferred color scheme | light, dark |
prefers-contrast | User’s preferred contrast level | no-preference, more, less, custom |
prefers-color-scheme Values
| Value | Description |
|---|---|
light | User prefers a light theme (or has no preference) |
dark | User prefers a dark theme |
prefers-contrast Values
| Value | Description |
|---|---|
no-preference | No specific preference (default) |
more | User prefers higher contrast |
less | User prefers lower contrast |
custom | User uses a specific set of colors/contrast |
WCAG Contrast Requirements
| Level | Normal Text | Large Text |
|---|---|---|
| AA | 4.5:1 | 3:1 |
| AAA | 7:1 | 4.5:1 |
Best Practices
✅ Do This:
/* Base light theme */
:root {
--bg: #ffffff;
--text: #212529;
--accent: #007bff;
}
/* Dark theme overrides */
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--text: #f8f9fa;
--accent: #4dabf7;
}
}
/* High contrast */
@media (prefers-contrast: more) {
:root {
--bg: #ffffff;
--text: #000000;
--accent: #0000cc;
}
section {
border: 2px solid #000;
box-shadow: none;
}
}
/* Combined dark + high contrast */
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
:root {
--bg: #000000;
--text: #ffffff;
--accent: #66b3ff;
}
}
❌ Don’t Do This:
/* Don't use pure black on pure white */
body {
background: #fff;
color: #000; /* Harsh — use #212529 instead */
}
/* Don't ignore user preferences */
body {
background: #fff;
color: #333;
/* No dark mode support — accessibility issue */
}
/* Don't use low contrast */
.note {
color: #ccc; /* On white — fails WCAG */
background: #fff;
}
/* Don't rely solely on color */
.error {
color: red; /* Add icons or text too */
}
Pro Tip: prefers-color-scheme and prefers-contrast are essential for accessibility. Always start with a light theme as the base, then enhance for dark mode. Use CSS custom properties for easy theming. Respect prefers-contrast: more by increasing contrast and adding borders. Remember: pure black on pure white is harsh — use off-black (#212529) and off-white (#f8f9fa) for better readability. Test with real users who have accessibility needs, and always ensure your text meets WCAG AA (4.5:1) or AAA (7:1) contrast ratios!
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!