Issue / Task Card

A Linear-style issue card with priority indicators, status icons, assignee avatars, labels, and keyboard selection. Perfect for project management and task tracking interfaces.

Tasks & IssuesInspired by Linear
Live PreviewInteractive
IssueCard.tsx
// Dependencies: react ^18, lucide-react
import React from 'react';
import { MessageSquare, Clock } from 'lucide-react';

type Label = {
  id: string;
  name: string;
  color: string;
};

type Issue = {
  id: string;
  identifier: string;
  title: string;
  status: 'backlog' | 'todo' | 'in-progress' | 'done' | 'cancelled';
  priority: 'urgent' | 'high' | 'medium' | 'low' | 'none';
  assignee?: { name: string; initials: string; color: string };
  labels?: Label[];
  dueDate?: Date;
  commentCount?: number;
  estimate?: number;
};

type Props = {
  issue: Issue;
  onClick?: () => void;
  selected?: boolean;
};

function StatusIcon({ status }: { status: Issue['status'] }) {
  if (status === 'backlog')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <circle cx="7" cy="7" r="6" stroke="#52525b" strokeWidth="1.5" strokeDasharray="3 2" />
      </svg>
    );
  if (status === 'todo')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <circle cx="7" cy="7" r="6" stroke="#71717a" strokeWidth="1.5" />
        <circle cx="7" cy="7" r="2" fill="#71717a" />
      </svg>
    );
  if (status === 'in-progress')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <circle cx="7" cy="7" r="6" stroke="#f59e0b" strokeWidth="1.5" />
        <path d="M7 1a6 6 0 0 1 0 12V7L7 1z" fill="#f59e0b" />
      </svg>
    );
  if (status === 'done')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <circle cx="7" cy="7" r="6" fill="#7c3aed" />
        <path d="M4.5 7l2 2 3-3" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    );
  return (
    <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
      <circle cx="7" cy="7" r="6" stroke="#3f3f46" strokeWidth="1.5" />
      <path d="M5 5l4 4M9 5l-4 4" stroke="#52525b" strokeWidth="1.5" strokeLinecap="round" />
    </svg>
  );
}

function PriorityIcon({ priority }: { priority: Issue['priority'] }) {
  if (priority === 'urgent')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <rect x="1" y="2" width="3" height="10" rx="1" fill="#ef4444" />
        <rect x="5.5" y="2" width="3" height="10" rx="1" fill="#ef4444" />
        <rect x="10" y="2" width="3" height="10" rx="1" fill="#ef4444" />
      </svg>
    );
  if (priority === 'high')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <rect x="1" y="5" width="3" height="7" rx="1" fill="#f97316" />
        <rect x="5.5" y="3" width="3" height="9" rx="1" fill="#f97316" />
        <rect x="10" y="1" width="3" height="11" rx="1" fill="#f97316" />
      </svg>
    );
  if (priority === 'medium')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <rect x="1" y="7" width="3" height="5" rx="1" fill="#eab308" />
        <rect x="5.5" y="4" width="3" height="8" rx="1" fill="#eab308" />
        <rect x="10" y="1" width="3" height="11" rx="1" fill="#3f3f46" />
      </svg>
    );
  if (priority === 'low')
    return (
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
        <rect x="1" y="9" width="3" height="3" rx="1" fill="#3b82f6" />
        <rect x="5.5" y="5" width="3" height="7" rx="1" fill="#3f3f46" />
        <rect x="10" y="1" width="3" height="11" rx="1" fill="#3f3f46" />
      </svg>
    );
  return <span className="w-3.5 h-0.5 rounded bg-zinc-700 inline-block" />;
}

function formatDue(date: Date): string {
  const now = new Date();
  const diff = date.getTime() - now.getTime();
  const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
  if (days === 0) return 'Today';
  if (days === 1) return 'Tomorrow';
  if (days < 0) return `${Math.abs(days)}d overdue`;
  return `${days}d`;
}

export function IssueCard({ issue, onClick, selected }: Props) {
  return (
    <button
      onClick={onClick}
      className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-left transition-colors border-b border-white/[0.05] last:border-0 group ${
        selected
          ? 'bg-indigo-500/[0.08] border-l-2 border-l-indigo-500 pl-[14px]'
          : 'hover:bg-white/[0.03]'
      }`}
    >
      <span className="shrink-0 flex items-center justify-center w-4">
        <PriorityIcon priority={issue.priority} />
      </span>
      <span className="shrink-0 flex items-center justify-center w-4">
        <StatusIcon status={issue.status} />
      </span>
      <span className="text-[11px] font-mono text-zinc-600 shrink-0 w-14">
        {issue.identifier}
      </span>
      <span className="flex-1 text-sm text-zinc-300 truncate">{issue.title}</span>
      {issue.labels && issue.labels.length > 0 && (
        <div className="flex items-center gap-1 shrink-0">
          {issue.labels.map((label) => (
            <span
              key={label.id}
              className="text-[10px] px-1.5 py-0.5 rounded-full font-medium border"
              style={{ color: label.color, borderColor: `${label.color}40`, backgroundColor: `${label.color}15` }}
            >
              {label.name}
            </span>
          ))}
        </div>
      )}
      {issue.dueDate && (
        <span className="flex items-center gap-1 text-[11px] text-zinc-500 shrink-0">
          <Clock className="w-3 h-3" />
          {formatDue(issue.dueDate)}
        </span>
      )}
      {issue.commentCount !== undefined && issue.commentCount > 0 && (
        <span className="flex items-center gap-1 text-[11px] text-zinc-600 shrink-0">
          <MessageSquare className="w-3 h-3" />
          {issue.commentCount}
        </span>
      )}
      {issue.estimate !== undefined && (
        <span className="text-[11px] text-zinc-600 shrink-0">{issue.estimate}pt</span>
      )}
      {issue.assignee && (
        <div
          className="w-5 h-5 rounded-full flex items-center justify-center text-[9px] font-bold text-white shrink-0"
          style={{ backgroundColor: issue.assignee.color }}
          title={issue.assignee.name}
        >
          {issue.assignee.initials}
        </div>
      )}
    </button>
  );
}

Unlock to copy

Free access to all patterns