Qure UI
Components

Task

How many tasks are attached to something, and how pressing they are.

66 tasks, including overdue and due soon
'use client'import { Task } from '@/registry/qure/ui/task'export default function TaskDemo() {  return <Task count={6} states={['overdue', 'soon']} />}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/task.json

Usage

import { Task } from '@/components/ui/task'
<Task count={6} states={['overdue', 'soon']} />

A count on its own answers how much, which is rarely the question anyone has. The dots answer how soon without spending a second column on it — a worklist row can say six tasks, two of them past due, in the width of a chip.

This is one of the components with no shadcn equivalent. It comes from the Qure design system.

Two axes, one pill

00 tasksnothing outstanding
33 tasks, including upcomingall more than a week out
44 tasks, including due soonsomething due within the week
22 tasks, including overduesomething past its date
66 tasks, including overdue and due soonboth, which is the common case
'use client'import { Task } from '@/registry/qure/ui/task'const cases = [  { states: [], count: 0, note: 'nothing outstanding' },  { states: ['upcoming'], count: 3, note: 'all more than a week out' },  { states: ['soon'], count: 4, note: 'something due within the week' },  { states: ['overdue'], count: 2, note: 'something past its date' },  { states: ['overdue', 'soon'], count: 6, note: 'both, which is the common case' },] as constexport default function TaskStates() {  return (    <div className="flex w-full max-w-md flex-col gap-3">      {cases.map(c => (        <div key={c.note} className="flex items-center gap-4">          <Task count={c.count} states={[...c.states]} />          <span className="text-body-small text-muted-foreground">{c.note}</span>        </div>      ))}    </div>  )}

count is the number. states is what is in there, and it is a set rather than an enum:

StateDotMeans
overdueurgentPast its date
soonattentionDue within the week
upcomingneutralMore than a week out

A pill with no states drops the dots and uses a plain document glyph. With dots, the glyph gains a notch at the top-right corner for them to sit in — that is two drawings in the Figma file, not one with an overlay, and it is why the dots never clip the document.

The Figma component ships its variants as urgent, variant2variant6, which name nothing. They resolve against the states the qtrack app actually computes — done, overdue, soon, upcoming. The tell is variant4, which carries a red dot and an amber one: that is not a fifth enum value, it is two flags at once, which is why states is an array.

done has no pill. A resolved task is not outstanding, so it does not count.

Order is not the caller's problem

66 tasks, including overdue and due soon['overdue', 'soon']
66 tasks, including overdue and due soon['soon', 'overdue']
'use client'import { Task } from '@/registry/qure/ui/task'/** * The dots are sorted by the component, not by the caller. Both pills below * are passed the same two states in opposite orders and render identically, * so a column of them lines up and the most pressing dot is always leftmost. */export default function TaskOrder() {  return (    <div className="flex flex-col items-start gap-3">      <div className="flex items-center gap-4">        <Task count={6} states={['overdue', 'soon']} />        <code className="text-label-base text-muted-foreground">['overdue', 'soon']</code>      </div>      <div className="flex items-center gap-4">        <Task count={6} states={['soon', 'overdue']} />        <code className="text-label-base text-muted-foreground">['soon', 'overdue']</code>      </div>    </div>  )}

The dots are sorted by the component. Passing ['soon', 'overdue'] renders the same thing as ['overdue', 'soon'], so a column of pills lines up and the most pressing dot is always leftmost. A component that renders in call order looks fine in isolation and ragged in a list.

In a worklist

  • RK
    Kaur R.PT-40192
    66 tasks, including overdue and due soon
  • MP
    Prasad M.PT-40188
    22 tasks, including due soon
  • JF
    Fernandes J.PT-40174
    33 tasks, including upcoming
  • SI
    Iyer S.PT-40160
    00 tasks
'use client'import { Avatar, AvatarFallback } from '@/registry/qure/ui/avatar'import { Task } from '@/registry/qure/ui/task'const rows = [  { name: 'Kaur R.', id: 'PT-40192', initials: 'RK', count: 6, states: ['overdue', 'soon'] },  { name: 'Prasad M.', id: 'PT-40188', initials: 'MP', count: 2, states: ['soon'] },  { name: 'Fernandes J.', id: 'PT-40174', initials: 'JF', count: 3, states: ['upcoming'] },  { name: 'Iyer S.', id: 'PT-40160', initials: 'SI', count: 0, states: [] },] as const/** * Where the two axes pay for themselves. Scanning down the column, the count * says how much work and the dots say how urgent, and neither needs a header * or a second column to be read. */export default function TaskWorklist() {  return (    <ul className="w-full max-w-sm">      {rows.map(row => (        <li key={row.id} className="hover:bg-accent/40 flex items-center gap-3 rounded-md px-3 py-2">          <Avatar size="sm">            <AvatarFallback>{row.initials}</AvatarFallback>          </Avatar>          <div className="flex min-w-0 flex-1 flex-col">            <span className="text-body-small-strong truncate">{row.name}</span>            <span className="text-label-base text-muted-foreground">{row.id}</span>          </div>          <Task count={row.count} states={[...row.states]} />        </li>      ))}    </ul>  )}

Where the two axes pay for themselves. Scanning down, the count says how much work and the dots say how urgent, and neither needs a column header to be read.

As an affordance

22 tasks, including overdue
'use client'import { Task } from '@/registry/qure/ui/task'/** * The pill as an affordance rather than a readout. `render` swaps the span for * a button, so it is focusable and announces as one; the styling follows * because the stylesheet keys the hover and focus ring off the element. */export default function TaskAdd() {  return (    <div className="flex items-center gap-3">      <Task count={2} states={['overdue']} />      <Task render={<button type="button" aria-label="Add a task" />}>+</Task>    </div>  )}

render swaps the span for a button, so it is focusable and announces as one. The hover and focus styles follow automatically — the stylesheet keys them off the element rather than off a prop:

<Task render={<button type="button" aria-label="Add a task" />}>+</Task>

children replaces the count when you pass it, which is how the + gets in. Give it an aria-label: a lone + is not a description of anything.

Accessibility

The glyph and its dots are one aria-hidden unit, and the pill carries a visually-hidden sentence instead:

<span class="sr-only">6 tasks, including overdue and due soon</span>

Announcing the dots individually would be three unlabelled circles. The sentence says what they mean, which is what the reader wants and what colour alone cannot carry — the whole point of having a count and dots is that the pill still works for someone who cannot tell the red from the amber.

API Reference

PropTypeDefaultDescription
countnumberHow many tasks. Ignored when children is given.
statesTaskState[][]"overdue", "soon", "upcoming". Order does not matter.
childrenReactNodeReplaces the count.
renderReactElement<span />Render as something else — a button, a link.

TASK_STATE_LABEL exports the wording used in the announcement, so a tooltip or a filter can reuse it rather than retyping it.

On this page