DisclosureRow

The one-line [leading] title · summary chrome that expands to reveal a body. It is the row shared by <r-reasoning> and <r-tool-card>, so a transcript carrying both has one disclosure language instead of two.

Use when you have a compact line that stands for something larger (a tool call, a chain of thought, a log group) and the detail is worth hiding until asked for.

Quick Start

Basic Usage

The body appears when the row is open.
<r-disclosure-row heading="Read file" summary="packages/ranui/index.ts" expandable>
  <div>The body appears when the row is open.</div>
</r-disclosure-row>

The heading is the fixed-width left half and the summary is the truncating right half, so a column of rows lines up on the same spine no matter how long each summary is. An empty summary drops the separator with it.

While the work is running

busy draws a shimmer sweep across the row. A spinner only indicates that something, somewhere, is happening; a sweep over the row identifies which row is still working.

With a leading indicator

The leading slot and the chevron share one grid cell, so swapping between them costs no layout and the heading never shifts under the pointer.

With nothing slotted into leading the chevron stays visible, since it is the only mark telling a reader the row opens. With leading content the chevron appears on hover, on focus or while open, and the state indicator is what shows the rest of the time.

Bundle exceeds the size limit.
<r-disclosure-row heading="Build" summary="failed in 4.2s" tone="error" expandable>
  <r-state-dot slot="leading" state="error"></r-state-dot>
  <div>Bundle exceeds the size limit.</div>
</r-disclosure-row>

API Reference

Properties

Property Attribute Type Default Description
heading heading string '' The fixed-width left half of the line.
summary summary string '' The truncating right half. Empty drops the separator with it.
open open boolean false Whether the body is shown. Reflected, so :has([open]) works.
expandable expandable boolean false Whether the row has a body worth opening.
busy busy boolean false Whether the work this row stands for is still running.
tone tone string '' error colours the summary; anything else is the ordinary tone.
name name string '' Groups rows so opening one closes the rest.
sheet sheet string '' CSS injected into the shadow root.

Events

Event Detail Dispatch Description
disclosurebeforetoggle { open: boolean } bubbles, composed, cancelable The row is about to be expanded or collapsed.
disclosuretoggle { open: boolean } bubbles, composed The row was expanded or collapsed.
row.addEventListener('disclosuretoggle', () => {
  console.log(row.open ? 'opened' : 'closed');
});

disclosurebeforetoggle fires first and can be refused, which is what makes "fetch the body the first time it is opened" and "refuse to collapse while an edit is unsaved" expressible. The platform has no equivalent: <details> fires only the after-the-fact toggle, and the request for a cancelable beforetoggle on it is still open.

row.addEventListener('disclosurebeforetoggle', async (event) => {
  if (!event.detail.open || row.dataset.loaded) return;
  event.preventDefault(); // hold it closed until the body is there
  row.append(await fetchBody());
  row.dataset.loaded = 'true';
  row.open = true;
});

Only a press fires it. A programmatic row.open = true is the application changing its own mind, and there is nobody for it to ask.

One row at a time

name groups rows the way name groups <details>: opening one closes the others. The group is the whole document, and the rows do not have to be siblings.

<r-disclosure-row name="run" heading="Install" expandable>…</r-disclosure-row>
<r-disclosure-row name="run" heading="Build" expandable>…</r-disclosure-row>
<r-disclosure-row name="run" heading="Test" expandable>…</r-disclosure-row>

Accessibility

A row is a control only when it has something to open. With expandable the row carries role="button", a tab stop, aria-expanded and aria-controls pointing at the body; without it the row carries none of them, because announcing a line of text as a button invites a press that does nothing. busy sets aria-busy, so the sweep is not the only signal that the work is still running.

A collapsed body is clipped rather than removed, so that it can animate. It is also made inert and its contents are skipped with content-visibility: hidden, which keeps it out of the tab order and off the render path while it is closed.

The row is 24px tall, which is the WCAG 2.5.8 minimum exactly, and rows stack with no gap. On a coarse pointer the default height goes to 32px, because the hit area cannot be grown past the row without overlapping the row above it, which trades a small target for a wrong one. Setting --ran-disclosure-row-height pins the height on every input type.

Slots

Slot Content
default The body, revealed while open.
leading An indicator before the heading, typically <r-state-dot>.
heading Markup for the left half, replacing the heading attribute's plain text.
summary Markup for the right half, replacing the summary attribute's plain text.

heading and summary take plain strings as attributes, which is what a tool-call row usually needs. When the half has to carry markup — code, a link, an abbreviation — slot it instead. The attribute text is the slot's fallback, so slotted content simply replaces it:

<r-disclosure-row expandable>
  <code slot="heading">fetch()</code>
  <a slot="summary" href="https://example.com">https://example.com</a>
  <pre>…</pre>
</r-disclosure-row>

Slotted content counts as one half of the line, so the separator appears and disappears the same way it does for the attributes.

Parts

row · leading · title · separator · summary · disclosure · body

Styling

<r-disclosure-row> exposes 15 CSS custom properties of its own, plus the semantic tokens it reads from the theme. Set one anywhere it inherits from, such as :root, a wrapper, or the element:

r-disclosure-row {
  --ran-disclosure-hover-background: var(--ran-color-bg-subtle);
}

Parts: body · disclosure · leading · row · separator · summary · title

The full list is in style tokens; which token to reach for is the design system.

Best Practices

  • Give a row a body, or don't make it expandable. A chevron that opens onto empty space serves no purpose; leave expandable off and the row stays a single line.
  • Keep the heading a fixed vocabulary (Read file, Run tests, Search) and put the variable part in the summary. That is what makes a column of rows scannable.
  • Pair tone="error" with words, never colour alone: the summary should say what failed.