RectangleSkeleton
Rectangular loading placeholder with a sweeping highlight, sized to the content it stands in for. One element per rectangle: a paragraph of three lines is three of these.
Use this when / not when
- Use when the shape of what is coming is known — a line of text, a button, a card, a table cell. The page does not jump when the content replaces it.
- Not when the duration is unknown and the shape is not:
Loaderis the spinner for that. - Not for a round placeholder — that is
CircleSkeleton, which takes a radius instead of a width and a height. - Not when the content is not coming at all. An empty list is
EmptyView, not a skeleton that never resolves.
Import
import { RectangleSkeleton } from "@onlyoffice/apps-ui-kit/components/rectangle";
Also exported from the root barrel @onlyoffice/apps-ui-kit.
No provider needed: the component draws itself in a fixed black at low opacity and reads nothing from the theme.
Stories
Default
A single placeholder with slightly rounded corners and the sweeping band; change any prop live in the Controls panel below.
Small Circle
A square with half its width as the corner radius turns into a circle (borderRadius), for an avatar placeholder that keeps the SSR-safe ids CircleSkeleton lacks.
Custom Colors
A light grey rectangle with a paler band, for a surface where the default black at low opacity does not read, such as a dark theme (backgroundColor, foregroundColor and their opacities).

No Animation
The same rectangle without the sweep, for a page that must not animate, such as one shown to a user who asked for reduced motion (animate).
Slow Animation
The band takes two and a half seconds to cross instead of two, a calmer pace for a large area (speed).
Grid
Six placeholders filling a three-column grid, each 100% of its cell's width, as a card grid shows while its items load.
Css Customization
There are no CSS variables to list on this page: the component reads none, as "Behaviour the types don't state" explains, so this example styles three pills of different widths with the same five props: backgroundColor, foregroundColor, their opacities and borderRadius.

Minimal example
width and height are the SVG's own size, so the skeleton is exactly as large as you say.
import { RectangleSkeleton } from "@onlyoffice/apps-ui-kit/components/rectangle";
export function TitlePlaceholder() {
return (
<RectangleSkeleton width="180px" height="22px" title="Loading the title" />
);
}
Props
Recipes
A block of text
Each line is its own element; the varying widths are what make it read as text.
import { RectangleSkeleton } from "@onlyoffice/apps-ui-kit/components/rectangle";
export function ParagraphPlaceholder() {
return (
<div
style={{ display: "flex", flexDirection: "column", gap: 8 }}
aria-busy="true"
>
<RectangleSkeleton width="100%" height="16px" title="Loading" />
<RectangleSkeleton width="80%" height="16px" />
<RectangleSkeleton width="55%" height="16px" />
</div>
);
}
A row of a list, with its avatar
import { CircleSkeleton } from "@onlyoffice/apps-ui-kit/components/circle";
import { RectangleSkeleton } from "@onlyoffice/apps-ui-kit/components/rectangle";
export function MemberRowPlaceholder() {
return (
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ width: 32, height: 32 }}>
<CircleSkeleton x="16" y="16" radius="16" width="32px" height="32px" />
</div>
<RectangleSkeleton width="140px" height="16px" />
</div>
);
}
Still, on a page that must not animate
import { RectangleSkeleton } from "@onlyoffice/apps-ui-kit/components/rectangle";
export function StillPlaceholder() {
return <RectangleSkeleton width="100%" height="32px" animate={false} />;
}
Behaviour the types don't state
- It has no accessible name by default. The underlying SVG carries
role="img"and anaria-labelledbythat points at the<title>— andtitledefaults to"", so no title is rendered and the id resolves to nothing. Give the skeleton that stands for a whole region atitle, or mark the regionaria-busyas the recipes do. - The colours are fixed, not themed.
backgroundColorandforegroundColorboth default to black at 10% and 15% opacity. On a dark surface that is nearly invisible. The component reads no theme variable of its own, but the props are written as the gradient stops'stop-colorattribute, and avar()there resolves against the page like any other: declare a pair under.lightand.darkand passbackgroundColor="var(--my-skeleton)", and the skeleton follows the theme as it switches. Choosing a hex in JavaScript from a dark-mode flag works too, but it is a second copy of the theme to keep in step. widthandheightsize both the element and the rectangle inside it. The SVG has noviewBox, sox,yandborderRadiusare in the element's own pixels. A skeleton withheight="100%"needs a parent with a height, or it collapses. A percentageborderRadiusis taken of the element's width, so"50%"on a square draws a circle.- The animation is one linear gradient sweeping across the shape for
speedseconds, forever, untilanimate={false}stops it; unmount the skeleton when the content arrives. uniqueKeyexists because the library picks a random id per render otherwise, which makes server and client markup differ. The component passes auseId()value when you leave it out, so the default is already SSR-safe —CircleSkeletonhas no such prop and is not.data-testidis written after the spread, so it cannot be overridden; anything else you pass lands on the<svg>.
Accessibility
role="img"comes from the library. Without atitleit is an unnamed image, which a screen reader announces as nothing at all.- A skeleton is not a live region. Mark the container
aria-busy="true"while it is showing, and move focus or announce the result yourself when the content arrives. animate={false}is the honest answer to a user who asked for reduced motion; the component does not checkprefers-reduced-motionon its own.
Test ids
It cannot be overridden by a prop, so several skeletons on a page share it — query them with
getAllByTestId.
Related
CircleSkeleton— the round placeholder, for avatars and icons.Loader— a spinner, for work whose shape is unknown.EmptyView— for when the content never arrives because there is none.