Adding Color and Bold to a SPA Task List: Design Decisions
When you're running a project management dashboard, the request eventually comes: "I want to highlight important tasks." This article walks through the design decisions behind adding per-task color and bold toggle to a custom SPA task list.
The Requirement: "Visually Distinguish Important Tasks"
With dozens of tasks on a management page, it's hard to spot what matters at a glance. Users wanted two things: custom colors and bold text for individual tasks.
Sounds simple, but there are real design choices to make.
Option 1: Add Properties Directly to Task Data
{
"id": "task-001",
"title": "Update API Documentation",
"done": false,
"color": "#e74c3c",
"bold": true
}
Pros: Simple. Data and display are unified.
Cons: Requires backend API and data structure changes. Introduces breaking changes to existing task data. Color and font weight are "display concerns," not "task essence."
Option 2: Manage Display Settings in a Separate Layer
// Store display settings in localStorage only
const taskStyles = JSON.parse(localStorage.getItem('taskStyles') || '{}');
// Example: { "task-001": { color: "#e74c3c", bold: true } }
Pros:
- Zero backend changes. No API modifications needed
- Preserves task data purity (done/not-done is the only server-side concern)
- Per-browser settings (no impact on teammates in collaborative use)
Cons:
- Settings lost when switching browsers
- No cross-device sync
For a personal dashboard, Option 2 was the clear winner.
Implementation: Context Menu Color Picker
Right-click (or long-press on mobile) on a task row to open a custom context menu. The reasoning:
- Showing a color picker on every task row would clutter the UI
- Right-click menus are the "there but not in the way" UI pattern
- If you're comfortable overriding the browser's default context menu, it's the cleanest approach
taskRow.addEventListener('contextmenu', (e) => {
e.preventDefault();
showStyleMenu(e.clientX, e.clientY, task.id);
});
function showStyleMenu(x, y, taskId) {
const menu = document.createElement('div');
menu.className = 'task-style-menu';
menu.innerHTML = `
<div class="color-row">
${['#e74c3c','#e67e22','#2ecc71','#3498db','#9b59b6','#1abc9c',''].map(c =>
`<span class="color-dot${c === '' ? ' clear' : ''}"
style="background:${c || '#ccc'}"
data-color="${c}"></span>`
).join('')}
</div>
<label class="bold-toggle">
<input type="checkbox" ${getTaskStyle(taskId).bold ? 'checked' : ''}>
<b>B</b> Bold
</label>
`;
menu.querySelectorAll('.color-dot').forEach(dot => {
dot.onclick = () => {
setTaskStyle(taskId, { color: dot.dataset.color });
applyTaskStyle(taskId);
menu.remove();
};
});
document.body.appendChild(menu);
menu.style.cssText = `position:fixed;left:${x}px;top:${y}px;z-index:9999`;
}
Gotcha: CSS Specificity Conflicts
Setting task colors via inline styles conflicts with existing CSS classes — like .done adding strikethrough and graying out text.
.task-item.done .task-title {
text-decoration: line-through;
color: #999;
opacity: 0.6;
}
The solution: overlay opacity on completed tasks with custom colors. The color remains visible, but the "finished" visual cue is preserved.
function applyTaskStyle(taskId) {
const el = document.querySelector(`[data-task-id="${taskId}"] .task-title`);
const style = getTaskStyle(taskId);
const isDone = el.closest('.task-item')?.classList.contains('done');
if (style.color) {
el.style.color = style.color;
if (isDone) el.style.opacity = '0.5';
} else {
el.style.color = '';
el.style.opacity = '';
}
el.style.fontWeight = style.bold ? 'bold' : '';
}
Another Gotcha: localStorage Bloat
When tasks are deleted, their style entries in localStorage linger. Garbage accumulates over time.
The fix: clean up orphaned entries whenever the task list renders.
function cleanupOrphanStyles(currentTaskIds) {
const styles = JSON.parse(localStorage.getItem('taskStyles') || '{}');
const idSet = new Set(currentTaskIds);
let changed = false;
for (const id of Object.keys(styles)) {
if (!idSet.has(id)) {
delete styles[id];
changed = true;
}
}
if (changed) localStorage.setItem('taskStyles', JSON.stringify(styles));
}
Design Retrospective
| Decision | Rationale |
|---|---|
| Separate display settings in localStorage | No backend API changes needed. Clean separation of concerns |
| Context menu approach | Zero UI pollution. Power-user friendly |
| Custom color + completed task coexistence | Opacity preserves "done" visual while keeping the color |
| Automatic orphan cleanup | Prevents localStorage bloat |
It's not a flashy feature, but the design philosophy of "keeping data pure while customizing display" applies broadly to SPA development. Data from your API and how it looks on screen should live in separate layers — your future self will thank you.
Top comments (0)