sveltelean Primitives
Versionv0.8.0 GitHub

Example

Payment ids in a table
CustomerPaymentAmount
Amara Okafor€120.00
Jonas Lindqvist€48.50
Mei Tanaka€310.25

<!-- The short form is the attribute; the whole value is the only text. No whitespace
     between the root and its part: the root's only rendered text is the short form. -->
<span data-slean-findable="…4d4d38"
	><span data-slean-part="full" hidden="until-found">tx-0123456789abcdef4d4d38</span></span
>
Search the page (Ctrl+F or Cmd+F) for 7f3a9c21: find reaches the whole id of the first payment, reveals it over its short form and highlights the match. Press Escape, or close the find bar, and it folds back. In a browser without hidden="until-found" (Safari) the short forms stay and find does not reach the whole ids.

Why this implementation exists

An operator table shows an opaque id by its last characters so the column stays narrow, but the operator searches the page for the whole id they were sent. A title attribute or a hint is not searched by find; a visually hidden copy is, but then the id is found twice and screen readers read both.

The platform has the answer: hidden="until-found" content is searched by find-in-page, and on a match the browser fires beforematch and reveals it. What it does not do is draw the revealed value where the short form was without moving the row, keep it inside a scrolling table, or fold it back when the operator moves on. That is what the behavior adds, through one beforematch handler registered with the shared router; the listeners that notice the end of a find exist only while a value is open (tests/findable.test.ts keeps one shared listener for a thousand values and adds none while idle).

The browser owns

  • find-in-page: searching the whole value under hidden="until-found", revealing it on a match, the highlight and the scroll to it
  • the same reveal for a fragment link to an element inside the value
  • the short form: CSS generated content, which find does not search, so the value is counted once

Svelte Lean owns

  • one shared beforematch listener for every findable value on the page; no state per element
  • one value open at a time: the next match folds the previous one back under hidden="until-found"
  • folding when find moves to another hidden match, on Escape, and when the find bar closes (the page takes its focus back with no press)
  • the shift that keeps the open value inside a scroller that would clip it, measured on the two frames after a match
  • slean:findend with the value and the reason; findable.css: the short form and the open value drawn over it

Usage

Install @svelte-lean/primitives for the behavior and @svelte-lean/styles for the stylesheet. With @svelte-lean/vite the registration is injected for every data-slean-findable attribute in the markup, with a static or a dynamic value; without it, import the register module once.

npm install @svelte-lean/primitives
+layout.svelte or any client module
import '@svelte-lean/primitives/findable/register';
stylesheets
import '@svelte-lean/styles/findable.css';

Put the short form in data-slean-findable and the whole value in a direct child <span data-slean-part="full" hidden="until-found">. Keep whitespace out between the two: the root’s only rendered text is the short form, drawn by findable.css, which is required.

Listen for slean:findend to close whatever you opened for the match; its reason is moved, escape, closed or code.

payments.svelte
<script lang="ts">
	import type { FindEndDetail } from '@svelte-lean/primitives/findable';

	let table: HTMLElement;

	$effect(() => {
		// slean:findend bubbles from the root once its value folded back: find moved on
		// ('moved'), Escape ('escape'), the find bar closed ('closed') or foldFound() ('code').
		const onfindend = (event: Event) => {
			const { value, reason } = (event as CustomEvent<FindEndDetail>).detail;
			console.log(`${value} folded: ${reason}`);
		};
		table.addEventListener('slean:findend', onfindend);
		return () => table.removeEventListener('slean:findend', onfindend);
	});
</script>

<table bind:this={table}>…</table>

Anatomy

PartElementdata-slean-partRequiredNotes
rootany element with data-slean-findable="…"–yesThe attribute value is the short form the stylesheet draws. Carries data-slean-found while its value is open. Copy and hint can sit on the same element.
full<span hidden="until-found">fullyesThe whole value, text only, as the root’s direct child. Out of the flow; drawn over the short form while open.

Runtime profile

The findable registers one beforematch handler with the shared router. On a match it marks the root data-slean-found, folds any other open value and starts a session: a capturing beforematch, pointerdown and keydown listener on the document and focus and blur on the window, removed together when the value folds. It reads layout on the two frames after a match (the computed overflow of the ancestors, the box of each one that clips, the value's box) to shift the value with an inline translate; nothing is read while idle or while the value stays open. The browser does not say when its find bar closes; a return of the page's focus with no press on the page is read as that.

Accessibility contract

  • Folded, assistive technology reads the short form: CSS generated content is part of the element’s text. The whole value is not rendered, so it is not in the accessibility tree; open, it is.
  • aria-label is prohibited on a generic element, so the behavior adds none. Where the whole value must reach assistive technology without find, give it where a name is allowed: aria-label on a copy <button>, as in the example below, or a hint with the whole value on a focusable root.
  • The behavior never moves focus. Escape on the page folds the open value without being consumed.

Keyboard

KeyWhenResult
Ctrl+F/Cmd+Fthe browser’s find barFind reaches the whole value, reveals it and scrolls to it
Escapeon the pageFolds the open value
Escapein the find barCloses the find bar; the page takes its focus back and the value folds

Platform features

FeatureBaselineOutside the target
hidden="until-found" and beforematchLimited availability: Chrome and Edge 102, Firefox 139; not in Safari as far as we knowThe value stays hidden (display: none); the short form shows and find does not reach the whole value
content: attr() on ::afterBaseline widely availableNot applicable
The translate propertyBaseline widely available since 2022The open value is not shifted inside a clipping scroller

Without JavaScript

Find still searches and reveals the whole value, and the stylesheet draws it over the short form; it stays open until the page reloads and no slean:findend is dispatched.

Server rendering

Render the markup as it is, including hidden="until-found". Nothing runs on the server or at import.

Before hydration

Before the behavior loads, find reveals a value and the stylesheet draws it, unshifted, and it stays open. The registration attaches no per-element state, so the first match after hydration is handled.

Styling

findable.css draws the short form (::after with content: attr(data-slean-findable)), takes the whole value out of the flow and, once it is open, draws it over the short form with the warning colors, a border and a shadow, starting where the short form starts so the row keeps its height. It never sets display on the value, so where hidden="until-found" is unknown the user agent's [hidden] rule keeps it hidden. The tokens it reads:

TokenDefault (light)Applies to
--slean-space-10.25remmargin-inline, padding-inline
--slean-warningoklch(76% 0.16 80)border
--slean-radius-sm0.375remborder-radius
--slean-warning-softoklch(96% 0.05 85)background
--slean-fgvar(--slean-neutral-12)color
--slean-shadow-md0 4px 12px oklch(0% 0 0 / 0.1), 0 1px 3px oklch(0% 0 0 / 0.08)box-shadow

State selectors the stylesheet targets, all from the platform or ARIA: [hidden].

Variant attributes: data-slean-findable.

Controlled integration

foldFound() folds the open value from code and dispatches slean:findend with reason code; foundRoot() returns the root whose value is open; supportsFindReveal() tells whether this browser searches and reveals hidden="until-found". shiftInside(box, bounds) is the pure shift the behavior applies.

view.ts
import { foldFound, foundRoot, supportsFindReveal } from '@svelte-lean/primitives/findable';

// Whether find-in-page can reach the whole values on this browser.
const findable = supportsFindReveal();

// Before a view is replaced: fold the value find opened (dispatches slean:findend, reason 'code').
if (foundRoot()) foldFound();

Compatibility notes

hidden="until-found" and beforematch are supported in Chrome and Edge 102 and Firefox 139, and not in Safari as far as we know; check the current status before relying on it. Where they are missing, the whole value is hidden like any hidden element, the short form shows, find does not reach the whole value and nothing runs. A fragment link to an element inside the value reveals it the same way. There is no find of its own and no fallback copy: a visually hidden copy would be found twice.

Examples

With copy and hint

The three attribute primitives on one button: find reaches the whole refund id, a click copies it, the hint shows it whole, and the button's name carries it for assistive technology. Search the page for 91e4b7a2, then click the opened id.

Refund

refund-id.svelte
<!-- Findable, copy and hint on one element: find reaches the whole id, a click copies it,
     the hint shows it whole and the button's name carries it for assistive technology. -->
<button type="button" class="id"
	data-slean-findable="…{id.slice(-6)}"
	data-slean-copy={id}
	data-slean-hint={id} data-slean-hint-mono data-slean-hint-copied="Copied"
	aria-label="Copy transaction {id}"
	><span data-slean-part="full" hidden="until-found">{id}</span></button
>

Cells that clip

The open value is shifted sideways to stay inside a scroller that would clip it, but it cannot be wider than the scroller. A cell that clips its own text for an ellipsis lets the value out while the root carries data-slean-found:

table.css
/* A cell that clips its text for an ellipsis lets the found value out while it is open. */
td:has(> [data-slean-found]) {
	overflow: visible;
}

Testing

Source