List
Hover list
A list with one bar sliding between its rows. It is a whole component rather than a hover style, because the bar has to know about rows it is not on.
Usage
import { HoverList } from '@carabine/ui/hover-list';nothing chosen
This list says where the pointer is, and that is all it says. There is no selected row — a second claim on the same surface leaves the list pointing at two things.
One bar
The bar is one element behind the rows, moved and resized to the row under the pointer. Rows are different heights when some carry a description and some do not, so both the position and the height are measured and filed under the row's id — which is why the id has to be stable across reorders.
On the first hover the bar is placed rather than animated. Sliding in from the top of the list on the first row you touch is the bar arriving; every hover after that is the bar travelling, which is the thing worth watching.
if (hovered === null) y.set(next); // first hover: place it
else animate(y, next, SPRING); // after that: travelExamples
On a plate
plate puts the toast's glass shell around the rows, for a list standing on its own rather than filling a panel. It is off by default: this is content more often than it is a container, and two panes of glass inside each other is the worse of the two mistakes.
Customization
<HoverList
items={items}
radius={12} // row corners, and the bar's
padding={12} // inside a row, both axes
gap={3} // between two rows
bar="rgba(255,255,255,0.07)" // a plain CSS string, not a class
plate
stiffness={420}
damping={38}
renderItem={(item) => <YourRow item={item} />} // the row stays ours
/>Styling reference
The bar is a tint: no hairline, no shadow, and letting the surface show through is the whole point. Its colour is the one plain CSS string in the component's API, because it is the thing a consumer is most likely to want in their own brand.
The type scale and the greys are the toast's, to the value — 13px medium over 12px regular. A list sitting inside a toast-shaped panel that used its own greys would read as a widget somebody dropped in.
API reference
| Prop | Type | |
|---|---|---|
| className | string | Added to the component’s own classes, so yours wins |
| style | CSSProperties | Merged after the component’s own inline styles, so yours wins |
Colours are data and dimensions are props, and neither covers a margin, a font, or a class from your own system. That is what these are for. State is legible from CSS as well — data-state, data-disabled, data-side — so a rule can answer it without knowing a single class name of ours. See Theming.
| Prop | Type | Default | |
|---|---|---|---|
| items | HoverItem[] | [] | |
| renderItem | (item, index) => ReactNode | — | Replaces a row’s contents; the row stays ours |
| onSelect | (item, index) => void | — | Also what turns the rows into buttons |
| radius | number | 10 | Row corners, and the bar’s |
| padding | number | 10 | Inside a row, both axes |
| gap | number | 2 | Between two rows |
| bar | string | theme's | The bar’s colour — a plain CSS string |
| plate | boolean | false | The toast’s glass shell around the rows |
| stiffness | number | 420 | |
| damping | number | 38 | |
| disabled | boolean | false | |
| label | string | — | Accessible name of the list |
| theme | 'dark' | 'light' | 'dark' |
A row's accessible name is everything in it — label and description. A description that is not part of the name is a description nobody hears.
What it refuses
No selected row
The bar answers one question, and it is where the pointer is. A list that also had to show a current value would need a second surface, and two surfaces claiming the same thing is the rule this component exists to demonstrate.
No virtualisation
Every row is measured, and a measured row that is not mounted has no height. A list long enough to need windowing is a table, and a table is a different component.