Infusion Loader
BetaLoaders · Feedback
A capsule with a soft slug — the only loader in the set that can tell the truth about how much is left.
5 states17 props16.4 KB installedsince 0.2.0
npx @oxygenui-design/cli add infusion-loaderFirst install? Run oxygen init once to say where your @/ alias points. The source is copied into your repository, along with anything it depends on.
Preview
Every state, switchable.
States4
Why this state exists
A real measurement: role=progressbar, a spoken value, and a slug that only moves when the number does. This is the only loader that should ever carry a percentage — and only when the application genuinely knows it.
Why it exists
Named for the one device in a hospital that displays a percentage and means it.
The two modes are deliberately different animations rather than one animation with a value bolted on: a determinate bar that also drifts tells a reader a measurement is moving when it is not, and on an import, a batch upload, or a records transfer, movement is exactly the fact being watched. It is the only loader here that should ever carry a number, and only when the application genuinely knows it — a fabricated percentage parked at ninety is worse than a loader that never claimed to know.
Usage
Props are the FHIR resource.
import { InfusionLoader } from "@/components/oxygen/infusion-loader";
// Determinate: the application knows how much is left
<InfusionLoader progress={42} label="Importing records" showLabel />
// Indeterminate: it does not, and says so by drifting
<InfusionLoader label="Preparing the export" />Dependencies
- clsx
- tailwind-merge
17 props
- actions
React.ReactNode
Rendered under the hint — a Retry or Go back control while someone waits.
- announce
LoaderAnnounce
Live-region politeness while indeterminate.
- delay
number
Wait this long before appearing, so a fast response never flashes a loader.
- hint
string
A second line under the label. Never a substitute for it.
- label
string
What is loading. Always rendered — visibly when `showLabel`, and to assistive technology either way, because a loader nobody can hear is a silent wait.
- minDuration
number
Once visible, stay at least this long, so the loader never blinks out.
- mode
LoaderMode
`inline` sits in the flow; `overlay` covers its positioned ancestor; `page` covers the viewport.
- motion
LoaderMotion
`auto` follows the OS; `reduced` forces the still state; `full` opts out of the OS preference.
- onSlow
(() => void)
Fires once, when `slowAfter` elapses.
- open
boolean
Controlled visibility. Setting false runs the exit and respects `minDuration`.
Page 1 of 2
Source
Exactly what lands in your repository.
Read directly from the published registry, so this can never drift from what the CLI installs.
Show sourceHide source89 lines
"use client";
/**
* InfusionLoader — a capsule with a soft slug, named for the one device in a
* hospital that displays a percentage and means it.
*
* The only loader in the set that can tell the truth about how much is left.
* Pass `progress` and it becomes a real 0–100 measurement with
* `role="progressbar"`; leave it off and the slug drifts end to end as an
* honest "unknown".
*
* The two modes are deliberately different animations, not one animation with a
* value bolted on. A determinate bar that also drifts is telling a reader that
* a measurement is moving when it is not — and on an import, a batch upload, or
* a records transfer, "moving" is the fact they are watching for.
*
* Use it only where progress is genuinely known. A fabricated percentage that
* sits at 90% is worse than a loader that never claimed to know.
*/
import * as React from "react";
import {
LOADER_VIEWBOX,
LoaderFrame,
clamp,
cycleMs,
resolveLoaderSize,
type LoaderCommonProps,
} from "@/lib/oxygen-loader";
/**
* Geometry of the capsule, in viewBox units.
*
* The slug never starts at zero width: 0% still has to look like a bar someone
* is watching rather than an empty track that failed to render.
*/
const TRACK_X = 12;
const SLUG_MIN = 30;
const SLUG_MAX = 136;
/** Indeterminate slug width — wide enough to read as a body of fluid, not a dot. */
const DRIFT_WIDTH = 48;
/** Width of the determinate slug at a given percentage. */
export function slugWidth(percent: number): number {
return SLUG_MIN + (clamp(percent, 0, 100) / 100) * (SLUG_MAX - SLUG_MIN);
}
export type InfusionLoaderProps = LoaderCommonProps;
export function InfusionLoader({ speed = 1, size, progress, ...props }: InfusionLoaderProps) {
const px = resolveLoaderSize(size, "xl");
const determinate = typeof progress === "number";
const value = determinate ? clamp(progress, 0, 100) : undefined;
return (
<LoaderFrame
{...props}
progress={progress}
variant="infusion"
vars={{
"--ox-loader-size": `${px}px`,
"--ox-loader-cycle": `${cycleMs(4000, clamp(speed, 0.5, 2))}ms`,
"--ox-loader-stroke": "2.4px",
}}
art={
<svg viewBox={LOADER_VIEWBOX.infusion} focusable="false">
<rect
className="ox-loader__stroke ox-loader__track"
x="4"
y="4"
width="152"
height="40"
rx="20"
/>
<rect
className="ox-loader__fill ox-loader__slug"
x={TRACK_X}
y="9"
width={determinate ? slugWidth(value ?? 0) : DRIFT_WIDTH}
height="30"
rx="15"
opacity="0.9"
/>
</svg>
}
/>
);
}