Icons laid out on a slowly turning sphere, built with CSS 3D transforms and no canvas. Each icon can be a link, a button or a plain image, and the list keeps one fixed order for the keyboard.
Optional, and kept apart.
Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.
import { IconCloud, BellIcon, CalendarIcon, ChartIcon, ClockIcon, FileIcon, HomeIcon, ImageIcon, InboxIcon, SearchIcon, SettingsIcon, StarIcon, TagIcon, UsersIcon } from "@rdloom/react";
const icons = [
["Notifications", BellIcon],
["Calendar", CalendarIcon],
["Reports", ChartIcon],
["Schedule", ClockIcon],
["Files", FileIcon],
["Home", HomeIcon],
["Images", ImageIcon],
["Inbox", InboxIcon],
["Search", SearchIcon],
["Settings", SettingsIcon],
["Favourites", StarIcon],
["Tags", TagIcon],
["People", UsersIcon],
] as const;
export default function IconCloudBasicExample() {
return (
<div className="flex w-full justify-center py-4">
<IconCloud
size={300}
iconSize={36}
label="Sample icons"
items={icons.map(([label, Icon]) => ({ label, icon: <Icon className="size-6 text-[var(--rd-color-text-default)]" /> }))}
/>
</div>
);
}Installation
npx rdloom add icon-cloudCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/icon-cloud.jsonWorks, but without upgrade tracking, and you import the motion CSS yourself.
Usage
import { IconCloud, BellIcon, CalendarIcon, ChartIcon, ClockIcon, FileIcon, HomeIcon, ImageIcon, InboxIcon, SearchIcon, SettingsIcon, StarIcon, TagIcon, UsersIcon } from "@rdloom/react";
<div className="flex w-full justify-center py-4">
<IconCloud
size={300}
iconSize={36}
label="Sample icons"
items={icons.map(([label, Icon]) => ({ label, icon: <Icon className="size-6 text-[var(--rd-color-text-default)]" /> }))}
/>
</div>Links
import { IconCloud } from "@rdloom/react";
const pages = ["Docs", "Guides", "Blog", "Pricing", "Status", "Team", "Jobs", "Help"];
export default function IconCloudLinksExample() {
return (
<div className="flex w-full justify-center py-4">
<IconCloud
size={300}
iconSize={48}
duration={60}
label="Sample pages"
items={pages.map((page) => ({
label: page,
href: "#",
icon: (
<span className="flex size-12 items-center justify-center rounded-full border border-[var(--rd-color-border-default)] bg-[var(--rd-color-surface-default)] text-[11px] text-[var(--rd-color-text-default)]">
{page}
</span>
),
}))}
/>
</div>
);
}Still
import { IconCloud, BellIcon, ChartIcon, HomeIcon, InboxIcon, SettingsIcon, StarIcon, UsersIcon } from "@rdloom/react";
const icons = [
["Notifications", BellIcon],
["Reports", ChartIcon],
["Home", HomeIcon],
["Inbox", InboxIcon],
["Settings", SettingsIcon],
["Favourites", StarIcon],
["People", UsersIcon],
] as const;
// isPaused keeps the sphere still, for pages that do not want any movement.
export default function IconCloudStillExample() {
return (
<div className="flex w-full justify-center py-4">
<IconCloud
isPaused
size={260}
iconSize={36}
label="Sample icons, still"
items={icons.map(([label, Icon]) => ({ label, icon: <Icon className="size-6 text-[var(--rd-color-text-default)]" /> }))}
/>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
itemsrequiredThe icons. label is the accessible name. An item with href is a link, one with onPress is a button, and any other is a plain icon. | Array<{ id?: string; label: string; icon: ReactNode; href?: string; onPress?: () => void }> | none |
sizeWidth and height of the sphere in pixels. | number | 320 |
iconSizeWidth and height of each icon in pixels. | number | 40 |
durationSeconds for one full turn. | number | 48 |
labelAccessible name of the list of icons. | string | "Icons" |
isPausedStops the turning. Moving content that lasts more than five seconds should be stoppable. | boolean | false |
Accessibility
Role list, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab: moves to each link or button in the order of the items array
- Enter: follows a link or presses a button; Space presses a button
Screen readers announce
- Announced as a list with the given label; each link, button or image is read in order with its label
What your code must do
- The icons are real list items in a fixed DOM order that never follows the rotation, so keyboard order is stable
- Each link and button has an accessible name from label; plain icons are images with that label
- The turning pauses while the pointer is over the cloud or focus is inside it, and with isPaused or the rdm-still class (WCAG 2.2.2)
- Visitors who ask for less motion (prefers-reduced-motion) get a still sphere
- Icons are kept facing the viewer while the sphere turns, so they are never mirrored or edge-on
- Focus shows a visible ring on the icon in every colour mode
- Does not flash: nothing changes more than three times in a second (WCAG 2.3.1)
Guidelines
Use it when
- Showing the range of tools or technologies a product supports
- A decorative hero graphic whose icons may also link somewhere
Avoid it when
- Navigation, because moving targets are hard to use
- Icons that need a visible text caption
Don't
- Icons that are only recognisable by colour
- Dozens of items, which crowd the sphere and overlap
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-focus-ring--rd-radius-control