Navigation

v0.0.4
Data display

Trace

What an agent or a job did, step by step, each a quiet line that opens its detail.

On this page

A trace shows what an agent or a job did, step by step: what it thought, searched, read and ran. Each step is one quiet line with a marker on a column, and a hairline joins the markers. A step with something behind it is a native <details>, so its line opens the detail with no script; a step with nothing behind it is a plain line.

Trace

<div class="card max-w(440) mx-auto">  <div class="trace">    <details class="trace-step">      <summary class="trace-summary">        <span class="trace-marker"></span>Thought about the migration        <span class="trace-meta">8s</span>      </summary>      <div class="trace-detail">The table has two million rows, so the column is added as nullable first and filled in batches.</div>    </details>    <details class="trace-step" open>      <summary class="trace-summary">        <span class="trace-marker"></span>Searched the codebase        <span class="trace-meta">12 files</span>      </summary>      <div class="trace-detail"><code class="code">orders.status</code> is read in 12 files, 3 of them in the billing service.</div>    </details>    <div class="trace-step">      <p class="trace-summary">        <span class="trace-marker"></span>Read schema.prisma        <span class="trace-meta">0.4s</span>      </p>    </div>    <div class="trace-step" aria-busy="true">      <p class="trace-summary">        <span class="trace-marker"></span>Writing the migration</p>    </div>  </div></div>

#A step

A step is a trace-step around a trace-summary: the marker, the words, and an optional trace-meta at the end of the line for a fact in figures, such as how long it took or how many it found.

When the step has a detail, write it as a <details> with the line on its <summary> and the detail in trace-detail after it. The stylesheet sees the detail and turns the line into a toggle: a small chevron follows the words and points down while the step is open. The detail opens to its own height where the browser can transition ::details-content, and at once elsewhere. Find-in-page opens a closed step that holds a match, as it does for any <details>.

#Markers

An empty trace-marker is a small dot. A marker may hold an icon instead, to say what kind of step it was, or a spinner with spinner-inline while the step runs.

Markers

<div class="card max-w(440) mx-auto">  <div class="trace">    <div class="trace-step">      <p class="trace-summary">        <span class="trace-marker">          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">            <circle cx="11" cy="11" r="6" />            <path d="m20 20-4.5-4.5" />          </svg>        </span>Searched for "rate limit"        <span class="trace-meta">4 results</span>      </p>    </div>    <div class="trace-step">      <p class="trace-summary">        <span class="trace-marker">          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">            <path d="M7 4h7l4 4v12H7zM14 4v4h4" />          </svg>        </span>Read limits.ts        <span class="trace-meta">214 lines</span>      </p>    </div>    <div class="trace-step">      <p class="trace-summary">        <span class="trace-marker">          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">            <path d="m5 8 4 4-4 4M12 17h7" />          </svg>        </span>Ran the tests        <span class="trace-meta">48 passed</span>      </p>    </div>    <div class="trace-step" aria-busy="true">      <p class="trace-summary">        <span class="trace-marker">          <span class="spinner spinner-inline"></span>        </span>Writing the answer</p>    </div>  </div></div>

#Running and failed

The step under way carries aria-busy="true": its words take the strong ink and a bare dot sends out a ring, which rests as a still halo for a reader who asks for reduced motion. Remove the attribute when the step ends.

A step that failed is trace-failed: its marker and the fact at the end of its line take the danger ink. It is a class because ARIA has no word for a failed item of a list. Say what failed in words, in the line or behind it, so the color is never the only sign.

Failed

<div class="card max-w(440) mx-auto">  <div class="trace">    <div class="trace-step">      <p class="trace-summary">        <span class="trace-marker"></span>Installed dependencies        <span class="trace-meta">14s</span>      </p>    </div>    <div class="trace-step">      <p class="trace-summary">        <span class="trace-marker"></span>Compiled 38 routes        <span class="trace-meta">48s</span>      </p>    </div>    <details class="trace-step trace-failed" open>      <summary class="trace-summary">        <span class="trace-marker"></span>Ran the test suite        <span class="trace-meta">2 failed</span>      </summary>      <div class="trace-detail">orders.spec.ts expected the status to be "paid" and received "pending", twice.</div>    </details>  </div></div>

#One line

A trace of one step is the line an assistant shows above its answer: what it did, and how long it took, with the reasoning behind it.

One step

<div class="trace max-w(440) mx-auto">  <details class="trace-step">    <summary class="trace-summary">      <span class="trace-marker"></span>Thought for 12 seconds</summary>    <div class="trace-detail">The question is about the fallback, not the feature, so the answer starts from a browser without anchor positioning.</div>  </details></div>

#Trace, timeline or steps

A trace is work: its steps may be running, may have failed, and open to show what stands behind them. A timeline is a record of events with their times. Steps are a fixed sequence a reader moves through.

#Accessibility

  • A step that opens is a <details>: the browser gives its line the role, the state and the keys of a disclosure.
  • The marker is drawn by the stylesheet and is not announced. An icon in it is decoration: say what the step was in the line.
  • A trace that grows while it is on screen should sit in a region with aria-live="polite", so each new step is announced once it is added. aria-busy on the running step marks it for assistive technology as well as for the eye.
  • On a touch screen every line is 44px tall.

#Classes

ClassDescription
traceThe steps, in a column; a hairline joins their markers
trace-stepOne step, on a details element when it has a detail and on any block when it has none
trace-failedA step that failed: its dot or icon and the fact at the end of its line take the danger ink. With trace-step
trace-summaryThe step's line, 32px tall: the marker, the words and a fact at the end. On a summary it is the toggle, with a chevron after the words
trace-markerThe marker at the start of the line: a dot when empty, or an icon, or a spinner-inline while the step runs
trace-metaA fact at the end of the line, in tabular figures: how long it took, how many it found
trace-detailWhat stands behind the step, under the line on the edge of its words

#Custom properties

PropertyDefaultDescription
--trace-marker1.25remWidth of the marker column

#Source

@master/ui/components/trace.css
/* * Trace: what an agent or a job did, step by step. * Each step is one quiet line with a marker on a column, and a hairline joins the markers. A step that has * something behind it (the query it ran, the lines it read, why it failed) is a native <details>: its line * is the <summary>, with a small chevron after the words, and the detail opens under it to its own height. * A step with nothing behind it is a plain line. * A bare marker is a dot; it may hold an icon instead, or a spinner-inline while the step runs. The step that is running carries * aria-busy: its words take the strong ink and its dot sends out a ring. A step that failed is trace-failed, * a class because ARIA has no word for it here; the reason, in words, is in the line or behind it. * Geometry: a 32px row, a 20px marker column, and the detail on the edge of the words. */@layer components {  .trace {    --trace-marker: 1.25rem;    --trace-padding: .375rem;    --trace-closed: -90deg;    display: grid;    min-width: 0;    margin: 0;    padding: 0;    color: var(--color-text-body);    font: 400 .84375rem/1.25rem var(--font-family-sans);    font-feature-settings: inherit;    list-style: none;    @media (pointer: coarse) {      --trace-padding: .75rem;    }  }  .trace-step {    position: relative;    min-width: 0;    /* Lets block-size travel from 0 to auto. */    interpolate-size: allow-keywords;    /* The line from under this marker to the next one. */    &:not(:last-child)::before {      content: '';      position: absolute;      top: calc(var(--trace-padding) + var(--trace-marker) + .125rem);      bottom: calc(-1 * var(--trace-padding) + .125rem);      inset-inline-start: calc(var(--trace-marker) / 2 - .5px);      width: 1px;      background: var(--color-line-divider);    }    &::details-content {      block-size: 0;      overflow-y: clip;      opacity: 0;      transition-property: block-size, opacity, content-visibility;      transition-duration: var(--duration-base);      transition-timing-function: var(--easing-glide);      transition-behavior: allow-discrete;    }    &[open]::details-content {      block-size: auto;      opacity: 1;    }    /* The chevron points down while its step is open. A closed step nested in an open one keeps its own. */    &[open] .trace-summary:has(~ .trace-detail)::after {      rotate: none;    }    &[open] .trace-step:not([open]) .trace-summary:has(~ .trace-detail)::after {      rotate: var(--trace-closed);    }    /* Running: the words come forward and a bare dot sends out a ring. */    &[aria-busy="true"] {      .trace-summary {        color: var(--color-text-strong);      }      .trace-marker:empty::before {        background: var(--color-text-strong);        animation: trace-live 1.8s var(--easing-glide) infinite;      }    }    /* Failed: the dot, an icon and the fact at the end of the line take the danger ink. */    &.trace-failed {      .trace-marker {        --icon-color: var(--color-danger);        color: var(--color-danger);      }      .trace-marker:empty::before {        background: var(--color-danger);      }      .trace-meta {        color: var(--color-danger);      }    }    @media (prefers-reduced-motion: reduce) {      &::details-content {        transition-property: opacity, content-visibility;      }      &[aria-busy="true"] .trace-marker:empty::before {        box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-text-strong) 12%, transparent);        animation: none;      }    }    @media (forced-colors: active) {      &::before {        background: CanvasText;      }    }  }  /* The step's line: the marker, the words, and a fact at the end. On a <summary> it is the toggle. */  .trace-summary {    display: flex;    align-items: flex-start;    gap: .5rem;    min-height: calc(var(--trace-marker) + var(--trace-padding) * 2);    margin: 0;    padding: var(--trace-padding) 0;    border-radius: calc((var(--radius-chip) - 2px) * var(--corner-scale));    corner-shape: var(--corner-shape);    list-style: none;    overflow-wrap: anywhere;    -webkit-tap-highlight-color: transparent;    &::-webkit-details-marker {      display: none;    }    /* With a detail after it, the line is a toggle: a chevron follows the words. */    &:has(~ .trace-detail) {      cursor: pointer;      touch-action: manipulation;      user-select: none;      -webkit-user-select: none;      &::after {        content: '';        order: 1;        flex: none;        width: .875rem;        height: .875rem;        margin-block: .1875rem;        margin-inline-start: -.25rem;        background: var(--color-text-muted);        mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4.5 6.5 3.5 3.5 3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;        rotate: var(--trace-closed);        transition: rotate var(--duration-base) var(--easing-glide);      }      /* Closed, the chevron points the way the words run. */      &:dir(rtl) {        --trace-closed: 90deg;      }      @media (hover: hover) {        &:hover {          color: var(--color-text-strong);        }        &:hover::after {          background: var(--color-text-strong);        }      }      /* Pressed: the strong ink, as under the pointer, so a tap is answered too. */      &:active {        color: var(--color-text-strong);      }      &:active::after {        background: var(--color-text-strong);      }    }    &:focus-visible {      @apply --focus-ring;    }    @media (prefers-reduced-motion: reduce) {      &::after {        transition: none;      }    }    @media (forced-colors: active) {      &::after {        background: CanvasText;      }    }  }  /* The marker's column, as tall as the first line of words. Empty, it is a dot. */  .trace-marker {    --icon-size: .875rem;    --icon-color: var(--color-text-muted);    display: grid;    flex: none;    place-items: center;    width: var(--trace-marker);    height: var(--trace-marker);    color: var(--color-text-muted);    &:empty::before {      content: '';      width: .375rem;      height: .375rem;      border-radius: 999px;      background: var(--color-line-mark);    }    @media (forced-colors: active) {      &:empty::before {        background: CanvasText;      }    }  }  /* A fact at the end of the line: how long it took, how many it found. */  .trace-meta {    order: 2;    flex: none;    margin-inline-start: auto;    padding-inline-start: .5rem;    color: var(--color-text-muted);    font-size: .78125rem;    font-variant-numeric: tabular-nums;  }  /* What stands behind the step, on the edge of its words. */  .trace-detail {    margin-inline-start: calc(var(--trace-marker) + .5rem);    padding: .125rem 0 .625rem;    color: var(--color-text-muted);    font-size: .8125rem;    line-height: 1.55;    text-wrap: pretty;    overflow-wrap: anywhere;  }}@keyframes trace-live {  from {    box-shadow: 0 0 0 0 color-mix(in oklab, var(--color-text-strong) 40%, transparent);  }  70%, to {    box-shadow: 0 0 0 5px transparent;  }}