Subtask Checklist

A checklist of subtasks under a parent task. Checking an item strikes through the label and updates a thin progress bar plus an 'X of Y' counter. An inline add-subtask row sits at the bottom.

Tasks & IssuesInspired by Linear
Live PreviewInteractive
Linear · MOD-418
Subtasks2 of 5

Click a subtask to toggle it · Type a label and press Enter to add one

SubtaskChecklist.tsx
// Dependencies: react ^18, lucide-react
import React, { useState } from 'react';
import { Check, Plus } from 'lucide-react';

function cn(...classes: (string | false | null | undefined)[]) {
  return classes.filter(Boolean).join(' ');
}

export type Subtask = {
  id: string;
  label: string;
  done: boolean;
};

type Props = {
  title?: string;
  initialSubtasks: Subtask[];
};

let nextId = 0;
const newId = () => `subtask-${Date.now()}-${++nextId}`;

export function SubtaskChecklist({ title = 'Subtasks', initialSubtasks }: Props) {
  const [subtasks, setSubtasks] = useState<Subtask[]>(initialSubtasks);
  const [draft, setDraft] = useState('');

  const completedCount = subtasks.filter((s) => s.done).length;
  const total = subtasks.length;
  const percent = total === 0 ? 0 : Math.round((completedCount / total) * 100);

  const toggle = (id: string) => {
    setSubtasks((prev) => prev.map((s) => (s.id === id ? { ...s, done: !s.done } : s)));
  };

  const addSubtask = () => {
    const label = draft.trim();
    if (!label) return;
    setSubtasks((prev) => [...prev, { id: newId(), label, done: false }]);
    setDraft('');
  };

  return (
    <div className="w-full max-w-sm bg-[#0a0a0a] border border-[#1a1a1a] rounded-xl overflow-hidden">
      {/* Header */}
      <div className="px-4 py-3 border-b border-[#1a1a1a]">
        <div className="flex items-center justify-between mb-2">
          <span className="text-sm font-semibold text-white">{title}</span>
          <span className="text-xs text-zinc-500">
            {completedCount} of {total}
          </span>
        </div>
        <div className="h-1 w-full bg-[#1e1e1e] rounded-full overflow-hidden">
          <div
            className="h-full bg-indigo-500 rounded-full transition-all duration-300"
            style={{ width: `${percent}%` }}
          />
        </div>
      </div>

      {/* Subtask rows */}
      <div className="flex flex-col">
        {subtasks.map((subtask) => (
          <button
            key={subtask.id}
            onClick={() => toggle(subtask.id)}
            className={cn(
              'flex items-center gap-3 px-4 py-2.5 text-left border-b border-[#1a1a1a] last:border-0 hover:bg-white/[0.03] transition-colors group'
            )}
          >
            <span
              className={cn(
                'shrink-0 w-4 h-4 rounded-full border flex items-center justify-center transition-colors',
                subtask.done
                  ? 'bg-indigo-500 border-indigo-500'
                  : 'border-zinc-600 group-hover:border-zinc-400'
              )}
            >
              {subtask.done && <Check className="w-2.5 h-2.5 text-white" strokeWidth={3} />}
            </span>
            <span
              className={cn(
                'text-sm flex-1 truncate',
                subtask.done ? 'text-zinc-600 line-through' : 'text-zinc-200'
              )}
            >
              {subtask.label}
            </span>
          </button>
        ))}
      </div>

      {/* Add subtask */}
      <div className="flex items-center gap-2 px-4 py-2.5 border-t border-[#1a1a1a]">
        <Plus className="w-3.5 h-3.5 text-zinc-600 shrink-0" />
        <input
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === 'Enter') addSubtask();
          }}
          placeholder="Add subtask"
          className="flex-1 bg-transparent text-sm text-zinc-200 placeholder:text-zinc-600 focus:outline-none"
        />
      </div>
    </div>
  );
}

// Usage:
// const SUBTASKS = [
//   { id: '1', label: 'Write spec', done: true },
//   { id: '2', label: 'Design API', done: true },
//   { id: '3', label: 'Implement backend', done: false },
//   { id: '4', label: 'Add tests', done: false },
//   { id: '5', label: 'Ship to staging', done: false },
// ];
// <SubtaskChecklist title="Subtasks" initialSubtasks={SUBTASKS} />