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
<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.
<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
expandableoff 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.