Where you are in a long page, in a small pill that appears once you start scrolling: a ring that fills as you read, the section you are in and how far through you are. Press it to jump to another section.
Installs components/ui/reading-progress.tsxDepends on lucide-reactMotion honours reduced motion
Scroll this panel: the pill appears and follows the section you are reading.
Why we changed matching
Payments in one currency now match invoices in another. Until now, a euro invoice paid into a sterling account sat unmatched until someone booked the difference by hand.
Payments in one currency now match invoices in another. Until now, a euro invoice paid into a sterling account sat unmatched until someone booked the difference by hand.
Payments in one currency now match invoices in another. Until now, a euro invoice paid into a sterling account sat unmatched until someone booked the difference by hand.
Which rate is used
The rate on the payment date is used, taken from the European Central Bank reference rates published that afternoon. If a payment lands on a weekend, the previous working day's rate applies.
The rate on the payment date is used, taken from the European Central Bank reference rates published that afternoon. If a payment lands on a weekend, the previous working day's rate applies.
The rate on the payment date is used, taken from the European Central Bank reference rates published that afternoon. If a payment lands on a weekend, the previous working day's rate applies.
Where the difference goes
The difference between the invoice amount at that rate and what arrived is posted to FX gains and losses as its own line, so it shows in reports and can be reviewed in the close.
The difference between the invoice amount at that rate and what arrived is posted to FX gains and losses as its own line, so it shows in reports and can be reviewed in the close.
The difference between the invoice amount at that rate and what arrived is posted to FX gains and losses as its own line, so it shows in reports and can be reviewed in the close.
Rules and IBAN matching
Rules can now match on IBAN as well as reference. A rule on IBAN is checked first, because a customer's bank account changes less often than how they write the reference.
Rules can now match on IBAN as well as reference. A rule on IBAN is checked first, because a customer's bank account changes less often than how they write the reference.
Rules can now match on IBAN as well as reference. A rule on IBAN is checked first, because a customer's bank account changes less often than how they write the reference.
What it does not do yet
Part payments across currencies are matched one at a time. Bulk matching treats foreign-currency lines as suggestions you confirm, never automatic matches.
Part payments across currencies are matched one at a time. Bulk matching treats foreign-currency lines as suggestions you confirm, never automatic matches.
Part payments across currencies are matched one at a time. Bulk matching treats foreign-currency lines as suggestions you confirm, never automatic matches.
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
Progress is derived, not stored
The ring and the section are read from the scroll position every frame the page moves, so they can never drift from what is on screen.
Jumping moves focus too
Choosing a section scrolls there and focuses it, so a keyboard or screen reader user continues reading from that section rather than from the pill.
Styling
Out of the way until you scroll
The pill appears after the first screen and floats over the page with the frosted surface, because it floats; it never covers the article's own outline.
Usage
One per scrolling surface
Use placement fixed for the window, or sticky as the last child of a scrolling panel with its ref passed as scrollRef.
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
import { useRef } from"react"import { ReadingProgress } from"@/components/ui/reading-progress"const chapters = [
{ id: "rp-why", title: "Why we changed matching", text: "Payments in one currency now match invoices in another. Until now, a euro invoice paid into a sterling account sat unmatched until someone booked the difference by hand." },
{ id: "rp-rate", title: "Which rate is used", text: "The rate on the payment date is used, taken from the European Central Bank reference rates published that afternoon. If a payment lands on a weekend, the previous working day's rate applies." },
{ id: "rp-difference", title: "Where the difference goes", text: "The difference between the invoice amount at that rate and what arrived is posted to FX gains and losses as its own line, so it shows in reports and can be reviewed in the close." },
{ id: "rp-rules", title: "Rules and IBAN matching", text: "Rules can now match on IBAN as well as reference. A rule on IBAN is checked first, because a customer's bank account changes less often than how they write the reference." },
{ id: "rp-limits", title: "What it does not do yet", text: "Part payments across currencies are matched one at a time. Bulk matching treats foreign-currency lines as suggestions you confirm, never automatic matches." },
];
exportfunction ReadingProgressDemo() {
const scroller = useRef<HTMLDivElement>(null);
return (
<div ref={scroller} className="h-[440px] w-full max-w-2xl overflow-auto rounded-[14px] px-6 pt-6 shadow-[inset_0_0_0_1px_var(--border)] max-sm:px-4">
<article className="grid gap-2">
<p className="font-mono text-xs text-muted-foreground">Release note</p>
<h2 className="text-2xl font-bold tracking-tight">Multi-currency matching</h2>
<p className="text-sm text-muted-foreground">Scroll this panel: the pill appears and follows the section you are reading.</p>
{chapters.map((chapter) => (
<section key={chapter.id} className="mt-6 grid gap-3">
<h3 id={chapter.id} className="scroll-mt-4 text-lg font-semibold outline-none">{chapter.title}</h3>
{[0, 1, 2].map((n) => <p key={n} className="text-sm leading-6">{chapter.text}</p>)}
</section>
))}
<div className="h-40" />
</article>
<ReadingProgress sections={chapters} scrollRef={scroller} placement="sticky" showAfter={60} />
</div>
);
}
sections *readonly ReadingSection[]—The sections in page order; each id is an element on the page.
scrollRefRefObject<HTMLElement | null>—The element that scrolls; the window when absent.
placement"sticky" | "fixed""fixed""fixed" floats at the bottom of the window; "sticky" sits at the bottom of its scrolling parent (render it last inside that parent).
showAfternumber120Pixels scrolled before the pill appears.