DEV Community

linou518
linou518

Posted on

Adding Color and Bold to a SPA Task List: Design Decisions

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
}
Enter fullscreen mode Exit fullscreen mode

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 } }
Enter fullscreen mode Exit fullscreen mode

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`;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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' : '';
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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)