useCollapsingLabel(ref, checkpoints, options?) β string| Param | Type | Description |
|---|---|---|
ref |
RefObject<HTMLElement \| null> |
Assigned to the button. Its computed font drives measurement. |
checkpoints |
string[] |
Labels widest β narrowest. End with '' for icon-only. |
options |
UseCollapsingLabelOptions |
Optional β containerRef (defaults to the buttonβs parent), iconSpace (24), reservedPadding (0), slack, deps. |
Returns the checkpoint string that currently fits.
const label = useCollapsingLabel(ref, ['Reformat resume', 'Reformat', ''], {
iconSpace: 20, // px for a leading icon + gap (default 24)
reservedPadding: 8, // extra safety margin trimmed from the budget
});
useCollapsingLabels(options) β number[]For a shared button row. options: containerRef, items ({ ref, variants }[]),
plus the same iconSpace / reservedPadding / slack / deps. Returns a level
index per item, parallel to items.
When several buttons live in one flex container and should divide the leftover room, use the plural hook. It returns a level index per button (map it to your own variant list). Buttons that arenβt mounted are skipped, so you can pass a fixed-order list even when some render conditionally:
const GENERATE = ['Download Profile (PDF)', 'Download Profile', 'Profile', ''];
const EDIT = ['Edit Latest Resume', 'Edit Resume', 'Edit', ''];
function ActionRow({ canEdit }: { canEdit: boolean }) {
const rowRef = useRef<HTMLDivElement>(null);
const genRef = useRef<HTMLButtonElement>(null);
const editRef = useRef<HTMLButtonElement>(null);
const [genLevel, editLevel] = useCollapsingLabels({
containerRef: rowRef,
items: [
{ ref: genRef, variants: GENERATE },
{ ref: editRef, variants: EDIT }, // editRef may be unmounted
],
deps: [canEdit], // re-measure when a button mounts/unmounts
});
return (
<div ref={rowRef} style=>
<button ref={genRef}>{GENERATE[genLevel] && <span>{GENERATE[genLevel]}</span>}</button>
{canEdit && <button ref={editRef}>{EDIT[editLevel] && <span>{EDIT[editLevel]}</span>}</button>}
</div>
);
}
The decision logic works on plain numbers β drive it from anywhere (a Web Component, a canvas UI, a test):
pickLevel(currentLevel, variantWidths, available, slack?) β numberpickLevelStep(...) β the single-step primitive pickLevel iterates.availableWidthPerItem({ containerWidth, totalGap, reservedWidth, truncatingCount, reservedPadding? }) β numberDEFAULT_SLACK β { shrinkSlack: 6, unshrinkSlack: 20 }.import { pickLevel, availableWidthPerItem } from 'dino-trunk';
const variantWidths = [180, 120, 60, 0]; // measured px, widest first
const available = availableWidthPerItem({
containerWidth: 640, totalGap: 16, reservedWidth: 40, truncatingCount: 3,
});
const level = pickLevel(currentLevel, variantWidths, available);
ellipsize(text, maxLength, ellipsis?) β stringA pure helper for the βcut a string and add β¦β cases:
import { ellipsize } from 'dino-trunk';
ellipsize('Hello, world', 8); // β 'Hello, β¦'
ellipsize('Hi', 8); // β 'Hi'
ellipsize('Hello, world', 8, '...'); // β 'Hello...' (ellipsis counts toward the budget)
iconSpace. The '' checkpoint measures to just
icon + padding β the natural floor.width + shrinkSlack), grow it back only once thereβs clearly room again
(width + unshrinkSlack). The gap between those is the anti-flicker dead-band.ResizeObserver (throttled to animation
frames); state updates only when a level actually changes.