Skip to main contentSkip to navigation

A line you open to read what is under it — material a screen holds but does not show first.

A screen usually holds more than it should show at once: the response a figure was read from, the selectors a call answered to. None of that belongs above the thing it explains, and none of it earns a page of its own. Fold is the line you open to read it.

Loading...

The behaviour is the element's

The browser already has this. details holds the open state, summary is the control, the keyboard reaches it with no tabIndex, and a screen reader says whether the fold is open before it reads the line. A box with a click handler gets none of that for free: it needs a role, aria-expanded, a tab stop, key handlers for Enter and Space, and state to hold what the element was holding already — and most of it is what gets written wrong. So the behaviour here is the element's, and what Fold adds is paint.

The elements themselves, rather than boxes rendered as them. A summary is a list-item, which is where its marker comes from, and that is not a display the style engine's boxes can spell, so the component writes details and summary and hands them style objects.

The summary stands at reach.dense, the rung inline material takes. A line at this type size stands about seventeen, so the vertical padding carries the rest and the target clears the 24 pixels WCAG 2.2 asks of anything offered to a finger.

What goes under the line carries the reading

Fold takes no reading. Table takes Rows<R> and Tally takes a Figure, because a component that has flattened "there is nothing" and "we could not ask" into an empty list cannot tell them apart afterwards.

type Rows<R> =
  | { at: 'reading' }
  | { at: 'read'; rows: R[] }
  | { at: 'failed'; why: string }

A fold holds whatever is put under it, so that distinction stays with the material and arrives inside the fold rather than around it.

<Fold summary="Selectors this precompile answered to">
  <Table
    name="selectors"
    columns={columns}
    id={(row) => row.selector}
    rows={selectors}
    empty={{ title: 'This precompile answered nothing' }}
  />
</Fold>

The summary line is the part to watch, because it is drawn whether or not the material under it arrived. A count in it is therefore a claim: Selectors (7) over a query that failed reports seven selectors nobody read. A summary that names the material stays true in all three branches, and the count belongs under the line, where the reading is.

Installation

npm install @luxfi/ui @hanzo/gui @hanzo/ui @hanzo/design @tanstack/react-query
import '@luxfi/ui/tokens.css'
import '@hanzo/design/styles.css'
import { AppProvider } from '@luxfi/ui/provider'
import { Fold } from '@luxfi/ui/read'

AppProvider goes once at the root of the app and mounts the gui engine and a query client. The two sheets are where the names resolve: colour from @luxfi/ui/tokens.css, spacing, elevation and measure from @hanzo/design.

Usage

import { Fold } from '@luxfi/ui/read'
<Fold summary="Raw response">
  <pre>{response}</pre>
</Fold>

open opens the fold on arrival, for the one somebody came to read.

<Fold summary="Selectors this precompile answered to" open>
  <SelectorList of={answered} />
</Fold>

The Lux exchange opens a fold in the seat screen, over the powers a seat acts through, and in the precompile screen, over the selectors a call answered to. Both open the same one, which is why this is a component rather than a pattern.

Props

PropTypeRequired
summaryReactNodeyes
asideReactNodeno
openbooleanno
childrenReactNodeno

summary is the line itself, and it is what the fold is called: besides the open state it is all a screen reader has to announce, so it has to read on its own with the fold shut. open is the element's own attribute, passed straight through. children render in a gui stack under the line, with $3 of space between them and above them.

aside is what is true of the material without opening it — how many rows are folded away, whether any of them needs attention. It sits at the far end of the line, in a track of its own, so a count that appears on one fold and not the next does not move where the names end. A fold without one draws no track and spends no gutter on it.

The frame is a card: the page's own ground inside a hairline, from paint, so a change of mind about either happens in the token layer rather than here.

What it does not do

Fold is paint on a native element, and it leaves the element's limits where they are. There is no onToggle and nothing to hold the open state against, so a caller learns nothing about whether the reader opened it and cannot keep a copy of that in step. Folds do not group: the name attribute that makes an exclusive set is not written, so opening one leaves its neighbours as they were. Nothing is deferred — children are in the document whether the fold is open or shut, which means the element hides the material rather than postponing it, and a child that reads on mount reads while the fold is closed. Nothing animates the reveal. And the component draws no distinction of its own between material that is absent and material that could not be read: an empty fold body is the same picture either way, which is the whole reason the components that go under it take a reading instead of a value.