UI Components Hard
Color Blind Modes
Color vision deficiency simulation with toggleable modes for protanopia, deuteranopia and tritanopia using SVG filters.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
body {
font-family: Inter, system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #e5e5e5;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 2rem;
}
.page {
width: 100%;
max-width: 780px;
}
.header {
margin-bottom: 1.5rem;
}
.header-title {
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 0.375rem;
}
.header-sub {
color: #737373;
font-size: 0.875rem;
line-height: 1.5;
}
/* ── Controls ── */
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.toggle-group {
display: flex;
background: #141414;
border: 1px solid #262626;
border-radius: 0.5rem;
overflow: hidden;
}
.mode-btn {
display: flex;
align-items: center;
gap: 0.375rem;
background: none;
border: none;
color: #a3a3a3;
padding: 0.5rem 0.875rem;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.mode-btn:hover {
background: #1a1a1a;
}
.mode-btn.active {
background: #262626;
color: #e5e5e5;
font-weight: 700;
}
.mode-icon {
font-weight: 800;
}
/* ── Switch toggle ── */
.version-toggle {
margin-left: auto;
}
.switch-label {
display: flex;
align-items: center;
gap: 0.625rem;
font-size: 0.8125rem;
color: #a3a3a3;
cursor: pointer;
}
.switch-label input {
display: none;
}
.switch-track {
width: 36px;
height: 20px;
background: #333;
border-radius: 10px;
position: relative;
transition: background 0.2s;
}
.switch-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: #e5e5e5;
border-radius: 50%;
transition: transform 0.2s;
}
.switch-label input:checked + .switch-track {
background: #22c55e;
}
.switch-label input:checked + .switch-track .switch-thumb {
transform: translateX(16px);
}
/* ── Dashboard ── */
.dashboard {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
transition: filter 0.3s;
}
/* SVG filter application */
.dashboard.filter-protanopia {
filter: url(#protanopia);
}
.dashboard.filter-deuteranopia {
filter: url(#deuteranopia);
}
.dashboard.filter-tritanopia {
filter: url(#tritanopia);
}
.panel {
background: #141414;
border: 1px solid #262626;
border-radius: 0.75rem;
padding: 1.25rem;
}
.panel-title {
font-size: 0.9375rem;
font-weight: 700;
margin-bottom: 1rem;
}
/* ── Status list ── */
.status-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.status-item {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.5rem 0;
border-bottom: 1px solid #1a1a1a;
}
.status-item:last-child {
border-bottom: none;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.status-green {
background: #22c55e;
}
.status-yellow {
background: #eab308;
}
.status-red {
background: #ef4444;
}
.status-icon {
display: none;
font-size: 1rem;
flex-shrink: 0;
}
.status-name {
flex: 1;
font-size: 0.875rem;
}
.status-label {
font-size: 0.6875rem;
font-weight: 600;
padding: 0.125rem 0.5rem;
border-radius: 1rem;
}
.status-label-green {
background: #052e16;
color: #4ade80;
}
.status-label-yellow {
background: #422006;
color: #fbbf24;
}
.status-label-red {
background: #450a0a;
color: #f87171;
}
/* ── Bar chart ── */
.chart {
padding: 0.5rem 0;
}
.chart-bars {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 180px;
gap: 1.5rem;
padding: 0 1rem;
margin-bottom: 1rem;
}
.bar-group {
display: flex;
align-items: flex-end;
gap: 0.25rem;
flex: 1;
height: 100%;
position: relative;
padding-bottom: 1.5rem;
}
.bar {
flex: 1;
border-radius: 0.25rem 0.25rem 0 0;
min-height: 4px;
position: relative;
transition: height 0.3s;
}
.bar-val {
position: absolute;
top: -1.25rem;
left: 50%;
transform: translateX(-50%);
font-size: 0.625rem;
font-weight: 600;
color: #a3a3a3;
white-space: nowrap;
}
.bar-organic {
background: #22c55e;
}
.bar-paid {
background: #ef4444;
}
.bar-referral {
background: #3b82f6;
}
.bar-label {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
font-size: 0.6875rem;
color: #737373;
}
.chart-legend {
display: flex;
gap: 1.25rem;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
color: #a3a3a3;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 0.125rem;
}
.legend-organic {
background: #22c55e;
}
.legend-paid {
background: #ef4444;
}
.legend-referral {
background: #3b82f6;
}
/* ── Alerts ── */
.alert-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.alert {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.875rem 1rem;
border-radius: 0.5rem;
border: 1px solid #262626;
font-size: 0.8125rem;
line-height: 1.5;
}
.alert strong {
display: block;
margin-bottom: 0.125rem;
}
.alert p {
color: #a3a3a3;
font-size: 0.75rem;
}
.alert-indicator {
width: 4px;
align-self: stretch;
border-radius: 2px;
flex-shrink: 0;
}
.alert-icon {
display: none;
font-size: 1rem;
flex-shrink: 0;
padding-top: 0.125rem;
}
.alert-success .alert-indicator {
background: #22c55e;
}
.alert-warning .alert-indicator {
background: #eab308;
}
.alert-error .alert-indicator {
background: #ef4444;
}
/* ── Accessible version: patterns + icons ── */
.accessible .status-dot {
display: none;
}
.accessible .status-icon {
display: inline;
}
.accessible .status-item:has(.status-green) .status-icon::before {
content: "\2713";
color: #4ade80;
} /* checkmark */
.accessible .status-item:has(.status-yellow) .status-icon::before {
content: "\26A0";
color: #fbbf24;
} /* warning */
.accessible .status-item:has(.status-red) .status-icon::before {
content: "\2717";
color: #f87171;
} /* x mark */
.accessible .bar-organic {
background: repeating-linear-gradient(45deg, #22c55e 0, #22c55e 4px, #16a34a 4px, #16a34a 8px);
}
.accessible .bar-paid {
background: repeating-linear-gradient(-45deg, #ef4444 0, #ef4444 4px, #dc2626 4px, #dc2626 8px);
}
.accessible .bar-referral {
background: repeating-linear-gradient(90deg, #3b82f6 0, #3b82f6 4px, #2563eb 4px, #2563eb 8px);
}
.accessible .legend-organic {
background: repeating-linear-gradient(45deg, #22c55e 0, #22c55e 2px, #16a34a 2px, #16a34a 4px);
}
.accessible .legend-paid {
background: repeating-linear-gradient(-45deg, #ef4444 0, #ef4444 2px, #dc2626 2px, #dc2626 4px);
}
.accessible .legend-referral {
background: repeating-linear-gradient(90deg, #3b82f6 0, #3b82f6 2px, #2563eb 2px, #2563eb 4px);
}
.accessible .alert-icon {
display: inline;
}
.accessible .alert-success .alert-icon::before {
content: "\2713";
color: #4ade80;
}
.accessible .alert-warning .alert-icon::before {
content: "\26A0";
color: #fbbf24;
}
.accessible .alert-error .alert-icon::before {
content: "\2717";
color: #f87171;
}
/* ── Info banner ── */
.info-banner {
background: #141414;
border: 1px solid #262626;
border-radius: 0.5rem;
padding: 0.75rem 1rem;
font-size: 0.8125rem;
color: #a3a3a3;
}
.info-banner strong {
color: #e5e5e5;
}
@media (max-width: 600px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.toggle-group {
flex-wrap: wrap;
}
.version-toggle {
margin-left: 0;
}
}(function () {
var dashboard = document.getElementById("dashboard");
var modeLabel = document.getElementById("mode-label");
var modeBtns = document.querySelectorAll(".mode-btn");
var accToggle = document.getElementById("accessible-toggle");
var modeNames = {
normal: "Normal Vision",
protanopia: "Protanopia (no red cones)",
deuteranopia: "Deuteranopia (no green cones)",
tritanopia: "Tritanopia (no blue cones)",
};
// ── Vision mode ──
function setVisionMode(mode) {
// Remove all filter classes
dashboard.classList.remove("filter-protanopia", "filter-deuteranopia", "filter-tritanopia");
// Apply new filter
if (mode !== "normal") {
dashboard.classList.add("filter-" + mode);
}
// Update button states
modeBtns.forEach(function (btn) {
var isActive = btn.getAttribute("data-mode") === mode;
btn.classList.toggle("active", isActive);
btn.setAttribute("aria-pressed", isActive ? "true" : "false");
});
// Update label
modeLabel.textContent = modeNames[mode] || "Normal Vision";
}
modeBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
setVisionMode(btn.getAttribute("data-mode"));
});
});
// ── Accessible version toggle ──
accToggle.addEventListener("change", function () {
if (accToggle.checked) {
dashboard.classList.add("accessible");
} else {
dashboard.classList.remove("accessible");
}
});
// ── Keyboard navigation for mode buttons ──
var toggleGroup = document.querySelector(".toggle-group");
toggleGroup.addEventListener("keydown", function (e) {
var btns = Array.from(modeBtns);
var current = document.querySelector(".mode-btn.active");
var idx = btns.indexOf(current);
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
e.preventDefault();
var next = btns[(idx + 1) % btns.length];
next.focus();
setVisionMode(next.getAttribute("data-mode"));
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
e.preventDefault();
var prev = btns[(idx - 1 + btns.length) % btns.length];
prev.focus();
setVisionMode(prev.getAttribute("data-mode"));
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Color Blind Modes</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- SVG filters for color vision deficiency simulation -->
<svg class="visually-hidden" aria-hidden="true">
<defs>
<!-- Protanopia (no red cones) -->
<filter id="protanopia">
<feColorMatrix type="matrix" values="
0.567, 0.433, 0, 0, 0
0.558, 0.442, 0, 0, 0
0, 0.242, 0.758, 0, 0
0, 0, 0, 1, 0" />
</filter>
<!-- Deuteranopia (no green cones) -->
<filter id="deuteranopia">
<feColorMatrix type="matrix" values="
0.625, 0.375, 0, 0, 0
0.7, 0.3, 0, 0, 0
0, 0.3, 0.7, 0, 0
0, 0, 0, 1, 0" />
</filter>
<!-- Tritanopia (no blue cones) -->
<filter id="tritanopia">
<feColorMatrix type="matrix" values="
0.95, 0.05, 0, 0, 0
0, 0.433, 0.567, 0, 0
0, 0.475, 0.525, 0, 0
0, 0, 0, 1, 0" />
</filter>
</defs>
</svg>
<div class="page">
<header class="header">
<h1 class="header-title">Color Blind Modes</h1>
<p class="header-sub">Simulate color vision deficiencies and compare accessible alternatives.</p>
</header>
<!-- Vision mode toggles -->
<div class="controls">
<div class="toggle-group" role="radiogroup" aria-label="Vision simulation">
<button class="mode-btn active" data-mode="normal" aria-pressed="true">
<span class="mode-icon">👁</span> Normal
</button>
<button class="mode-btn" data-mode="protanopia" aria-pressed="false">
<span class="mode-icon">P</span> Protanopia
</button>
<button class="mode-btn" data-mode="deuteranopia" aria-pressed="false">
<span class="mode-icon">D</span> Deuteranopia
</button>
<button class="mode-btn" data-mode="tritanopia" aria-pressed="false">
<span class="mode-icon">T</span> Tritanopia
</button>
</div>
<div class="version-toggle">
<label class="switch-label">
<span>Show accessible version</span>
<input type="checkbox" id="accessible-toggle" />
<span class="switch-track"><span class="switch-thumb"></span></span>
</label>
</div>
</div>
<div class="dashboard" id="dashboard">
<!-- Status indicators -->
<div class="panel">
<h2 class="panel-title">System Status</h2>
<div class="status-list" id="status-list">
<div class="status-item">
<span class="status-dot status-green" aria-hidden="true"></span>
<span class="status-icon" aria-hidden="true"></span>
<span class="status-name">API Gateway</span>
<span class="status-label status-label-green">Operational</span>
</div>
<div class="status-item">
<span class="status-dot status-green" aria-hidden="true"></span>
<span class="status-icon" aria-hidden="true"></span>
<span class="status-name">Database</span>
<span class="status-label status-label-green">Operational</span>
</div>
<div class="status-item">
<span class="status-dot status-yellow" aria-hidden="true"></span>
<span class="status-icon" aria-hidden="true"></span>
<span class="status-name">CDN</span>
<span class="status-label status-label-yellow">Degraded</span>
</div>
<div class="status-item">
<span class="status-dot status-red" aria-hidden="true"></span>
<span class="status-icon" aria-hidden="true"></span>
<span class="status-name">Email Service</span>
<span class="status-label status-label-red">Down</span>
</div>
<div class="status-item">
<span class="status-dot status-green" aria-hidden="true"></span>
<span class="status-icon" aria-hidden="true"></span>
<span class="status-name">Auth Server</span>
<span class="status-label status-label-green">Operational</span>
</div>
</div>
</div>
<!-- Bar chart -->
<div class="panel">
<h2 class="panel-title">Monthly Traffic</h2>
<div class="chart">
<div class="chart-bars">
<div class="bar-group">
<div class="bar bar-organic" style="height: 70%"><span class="bar-val">14k</span></div>
<div class="bar bar-paid" style="height: 45%"><span class="bar-val">9k</span></div>
<div class="bar bar-referral" style="height: 25%"><span class="bar-val">5k</span></div>
<span class="bar-label">Jan</span>
</div>
<div class="bar-group">
<div class="bar bar-organic" style="height: 80%"><span class="bar-val">16k</span></div>
<div class="bar bar-paid" style="height: 50%"><span class="bar-val">10k</span></div>
<div class="bar bar-referral" style="height: 30%"><span class="bar-val">6k</span></div>
<span class="bar-label">Feb</span>
</div>
<div class="bar-group">
<div class="bar bar-organic" style="height: 65%"><span class="bar-val">13k</span></div>
<div class="bar bar-paid" style="height: 55%"><span class="bar-val">11k</span></div>
<div class="bar bar-referral" style="height: 35%"><span class="bar-val">7k</span></div>
<span class="bar-label">Mar</span>
</div>
<div class="bar-group">
<div class="bar bar-organic" style="height: 90%"><span class="bar-val">18k</span></div>
<div class="bar bar-paid" style="height: 60%"><span class="bar-val">12k</span></div>
<div class="bar bar-referral" style="height: 40%"><span class="bar-val">8k</span></div>
<span class="bar-label">Apr</span>
</div>
</div>
<div class="chart-legend" id="chart-legend">
<span class="legend-item"><span class="legend-dot legend-organic"></span> Organic</span>
<span class="legend-item"><span class="legend-dot legend-paid"></span> Paid</span>
<span class="legend-item"><span class="legend-dot legend-referral"></span> Referral</span>
</div>
</div>
</div>
<!-- Alert banners -->
<div class="panel">
<h2 class="panel-title">Alerts</h2>
<div class="alert-list">
<div class="alert alert-success">
<span class="alert-indicator" aria-hidden="true"></span>
<span class="alert-icon" aria-hidden="true"></span>
<div>
<strong>Deployment successful</strong>
<p>Build #4827 deployed to production at 14:32 UTC.</p>
</div>
</div>
<div class="alert alert-warning">
<span class="alert-indicator" aria-hidden="true"></span>
<span class="alert-icon" aria-hidden="true"></span>
<div>
<strong>High memory usage</strong>
<p>Server mem-03 is at 92% memory utilization.</p>
</div>
</div>
<div class="alert alert-error">
<span class="alert-indicator" aria-hidden="true"></span>
<span class="alert-icon" aria-hidden="true"></span>
<div>
<strong>Failed health check</strong>
<p>Endpoint /api/health returned 503 for 5 minutes.</p>
</div>
</div>
</div>
</div>
</div>
<div class="info-banner" id="info-banner">
<strong>Current mode:</strong> <span id="mode-label">Normal Vision</span>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Simulates color vision deficiencies (protanopia, deuteranopia, tritanopia) using SVG color matrix filters applied in real-time. Includes a dashboard-like UI with colored status indicators and charts, plus an accessible alternative that uses patterns, icons, and text labels instead of relying on color alone.