UI Components Medium
Icon Cloud
A 3D tag cloud of technology icons orbiting in space using Fibonacci sphere positioning and smooth rotation animation.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
background: #0a0a0a;
overflow: hidden;
}
.cloud-wrapper {
position: relative;
width: 100%;
height: 100vh;
display: grid;
place-items: center;
background: #0a0a0a;
overflow: hidden;
}
.icon-cloud {
position: relative;
width: 500px;
height: 500px;
max-width: 90vmin;
max-height: 90vmin;
}
.icon-cloud .cloud-icon {
position: absolute;
left: 50%;
top: 50%;
font-size: 2rem;
line-height: 1;
transform-origin: center;
transition: opacity 0.1s ease;
pointer-events: none;
user-select: none;
filter: grayscale(0.2);
will-change: transform, opacity;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(4px);
}
.cloud-content {
position: absolute;
bottom: 10%;
left: 0;
right: 0;
text-align: center;
pointer-events: none;
z-index: 10;
}
.cloud-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #c4b5fd 0%, #8b5cf6 50%, #7c3aed 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.cloud-subtitle {
font-size: clamp(0.875rem, 2vw, 1.125rem);
color: rgba(148, 163, 184, 0.8);
font-weight: 400;
}// Icon Cloud — 3D rotating sphere of icons
(function () {
"use strict";
const container = document.getElementById("icon-cloud");
if (!container) return;
const ICONS = [
"\u269B\uFE0F", // React
"\u{1F310}", // Globe (Web)
"\u26A1", // Zap (Vite)
"\u{1F4E6}", // Package (npm)
"\u{1F680}", // Rocket (Deploy)
"\u{1F3A8}", // Art (CSS)
"\u{1F527}", // Wrench (Tools)
"\u{1F4BB}", // Laptop (Dev)
"\u2728", // Sparkles
"\u{1F50D}", // Search
"\u{1F4CA}", // Chart
"\u{1F512}", // Lock (Security)
"\u2601\uFE0F", // Cloud
"\u{1F916}", // Robot (AI)
"\u{1F9E9}", // Puzzle
"\u{1F4A1}", // Lightbulb
"\u{1F525}", // Fire
"\u{1F48E}", // Gem
"\u{1F3AF}", // Target
"\u{1F30A}", // Wave
];
const ROTATE_SPEED = 0.004;
const PERSPECTIVE = 500;
const TILT_SENSITIVITY = 0.0003;
let rotY = 0;
let rotX = 0;
let targetTiltX = 0;
let targetTiltY = 0;
let tiltX = 0;
let tiltY = 0;
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
const count = ICONS.length;
// Generate sphere points
const points = [];
for (let i = 0; i < count; i++) {
const y = 1 - (i / (count - 1)) * 2;
const r = Math.sqrt(1 - y * y);
const theta = goldenAngle * i;
points.push({
x: Math.cos(theta) * r,
y: y,
z: Math.sin(theta) * r,
});
}
// Create DOM elements
const elements = ICONS.map((icon) => {
const el = document.createElement("div");
el.className = "cloud-icon";
el.textContent = icon;
container.appendChild(el);
return el;
});
// Mouse tilt
container.addEventListener("mousemove", (e) => {
const rect = container.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
targetTiltY = (e.clientX - cx) * TILT_SENSITIVITY;
targetTiltX = (e.clientY - cy) * TILT_SENSITIVITY;
});
container.addEventListener("mouseleave", () => {
targetTiltX = 0;
targetTiltY = 0;
});
function rotatePoint(p, ry, rx) {
let x = p.x * Math.cos(ry) - p.z * Math.sin(ry);
let z = p.x * Math.sin(ry) + p.z * Math.cos(ry);
let y2 = p.y * Math.cos(rx) - z * Math.sin(rx);
let z2 = p.y * Math.sin(rx) + z * Math.cos(rx);
return { x, y: y2, z: z2 };
}
const size = container.clientWidth / 2;
function tick() {
rotY += ROTATE_SPEED;
// Smooth tilt interpolation
tiltX += (targetTiltX - tiltX) * 0.05;
tiltY += (targetTiltY - tiltY) * 0.05;
const totalRotX = rotX + tiltX;
const totalRotY = rotY + tiltY;
for (let i = 0; i < count; i++) {
const rotated = rotatePoint(points[i], totalRotY, totalRotX);
const scale = PERSPECTIVE / (PERSPECTIVE + rotated.z * size);
const px = rotated.x * size * scale;
const py = rotated.y * size * scale;
const opacity = Math.max(0.15, (rotated.z + 1) / 2);
const s = 0.6 + scale * 0.5;
elements[i].style.transform = `translate(-50%, -50%) translate(${px}px, ${py}px) scale(${s})`;
elements[i].style.opacity = opacity;
elements[i].style.zIndex = Math.round(scale * 100);
}
requestAnimationFrame(tick);
}
tick();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Icon Cloud</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="cloud-wrapper">
<div id="icon-cloud" class="icon-cloud"></div>
<div class="cloud-content">
<h1 class="cloud-title">Icon Cloud</h1>
<p class="cloud-subtitle">Technology icons orbiting in 3D space</p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useRef, useEffect, useMemo } from "react";
interface IconCloudProps {
icons?: string[];
rotateSpeed?: number;
perspective?: number;
tiltSensitivity?: number;
className?: string;
}
interface Point3D {
x: number;
y: number;
z: number;
}
const DEFAULT_ICONS = [
"\u269B\uFE0F",
"\u{1F310}",
"\u26A1",
"\u{1F4E6}",
"\u{1F680}",
"\u{1F3A8}",
"\u{1F527}",
"\u{1F4BB}",
"\u2728",
"\u{1F50D}",
"\u{1F4CA}",
"\u{1F512}",
"\u2601\uFE0F",
"\u{1F916}",
"\u{1F9E9}",
"\u{1F4A1}",
"\u{1F525}",
"\u{1F48E}",
"\u{1F3AF}",
"\u{1F30A}",
];
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
function generateSpherePoints(count: number): Point3D[] {
const pts: Point3D[] = [];
for (let i = 0; i < count; i++) {
const y = 1 - (i / (count - 1)) * 2;
const r = Math.sqrt(1 - y * y);
const theta = GOLDEN_ANGLE * i;
pts.push({ x: Math.cos(theta) * r, y, z: Math.sin(theta) * r });
}
return pts;
}
function rotatePoint(p: Point3D, ry: number, rx: number): Point3D {
const x = p.x * Math.cos(ry) - p.z * Math.sin(ry);
const z = p.x * Math.sin(ry) + p.z * Math.cos(ry);
const y2 = p.y * Math.cos(rx) - z * Math.sin(rx);
const z2 = p.y * Math.sin(rx) + z * Math.cos(rx);
return { x, y: y2, z: z2 };
}
export function IconCloud({
icons = DEFAULT_ICONS,
rotateSpeed = 0.004,
perspective = 500,
tiltSensitivity = 0.0003,
className = "",
}: IconCloudProps) {
const containerRef = useRef<HTMLDivElement>(null);
const elementsRef = useRef<HTMLDivElement[]>([]);
const rotRef = useRef({ y: 0, x: 0 });
const tiltRef = useRef({ x: 0, y: 0, targetX: 0, targetY: 0 });
const points = useMemo(() => generateSpherePoints(icons.length), [icons.length]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let animId: number;
const onMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
tiltRef.current.targetY = (e.clientX - cx) * tiltSensitivity;
tiltRef.current.targetX = (e.clientY - cy) * tiltSensitivity;
};
const onLeave = () => {
tiltRef.current.targetX = 0;
tiltRef.current.targetY = 0;
};
container.addEventListener("mousemove", onMove);
container.addEventListener("mouseleave", onLeave);
function tick() {
const size = container!.clientWidth / 2;
rotRef.current.y += rotateSpeed;
const t = tiltRef.current;
t.x += (t.targetX - t.x) * 0.05;
t.y += (t.targetY - t.y) * 0.05;
const totalRY = rotRef.current.y + t.y;
const totalRX = rotRef.current.x + t.x;
for (let i = 0; i < points.length; i++) {
const el = elementsRef.current[i];
if (!el) continue;
const rotated = rotatePoint(points[i], totalRY, totalRX);
const scale = perspective / (perspective + rotated.z * size);
const px = rotated.x * size * scale;
const py = rotated.y * size * scale;
const opacity = Math.max(0.15, (rotated.z + 1) / 2);
const s = 0.6 + scale * 0.5;
el.style.transform = `translate(-50%, -50%) translate(${px}px, ${py}px) scale(${s})`;
el.style.opacity = String(opacity);
el.style.zIndex = String(Math.round(scale * 100));
}
animId = requestAnimationFrame(tick);
}
tick();
return () => {
cancelAnimationFrame(animId);
container.removeEventListener("mousemove", onMove);
container.removeEventListener("mouseleave", onLeave);
};
}, [points, rotateSpeed, perspective, tiltSensitivity]);
return (
<div
ref={containerRef}
className={className}
style={{
position: "relative",
width: 500,
height: 500,
maxWidth: "90vmin",
maxHeight: "90vmin",
}}
>
{icons.map((icon, i) => (
<div
key={i}
ref={(el) => {
if (el) elementsRef.current[i] = el;
}}
style={{
position: "absolute",
left: "50%",
top: "50%",
fontSize: "2rem",
lineHeight: 1,
pointerEvents: "none",
userSelect: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 48,
height: 48,
background: "rgba(255, 255, 255, 0.05)",
borderRadius: 12,
border: "1px solid rgba(255, 255, 255, 0.08)",
backdropFilter: "blur(4px)",
willChange: "transform, opacity",
}}
>
{icon}
</div>
))}
</div>
);
}
// Demo usage
export default function IconCloudDemo() {
return (
<div
style={{
width: "100vw",
height: "100vh",
background: "#0a0a0a",
display: "grid",
placeItems: "center",
position: "relative",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<IconCloud />
<div
style={{
position: "absolute",
bottom: "10%",
left: 0,
right: 0,
textAlign: "center",
pointerEvents: "none",
zIndex: 10,
}}
>
<h1
style={{
fontSize: "clamp(2rem, 5vw, 3.5rem)",
fontWeight: 800,
letterSpacing: "-0.03em",
background: "linear-gradient(135deg, #c4b5fd 0%, #8b5cf6 50%, #7c3aed 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
marginBottom: "0.5rem",
}}
>
Icon Cloud
</h1>
<p
style={{
fontSize: "clamp(0.875rem, 2vw, 1.125rem)",
color: "rgba(148, 163, 184, 0.8)",
}}
>
Technology icons orbiting in 3D space
</p>
</div>
</div>
);
}<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
const props = defineProps({
icons: {
type: Array,
default: () => [
"\u269B\uFE0F",
"\u{1F310}",
"\u26A1",
"\u{1F4E6}",
"\u{1F680}",
"\u{1F3A8}",
"\u{1F527}",
"\u{1F4BB}",
"\u2728",
"\u{1F50D}",
"\u{1F4CA}",
"\u{1F512}",
"\u2601\uFE0F",
"\u{1F916}",
"\u{1F9E9}",
"\u{1F4A1}",
"\u{1F525}",
"\u{1F48E}",
"\u{1F3AF}",
"\u{1F30A}",
],
},
rotateSpeed: { type: Number, default: 0.004 },
perspective: { type: Number, default: 500 },
tiltSensitivity: { type: Number, default: 0.0003 },
});
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
const containerEl = ref(null);
const iconEls = ref([]);
let animId;
const rot = { y: 0, x: 0 };
const tilt = { x: 0, y: 0, targetX: 0, targetY: 0 };
function generateSpherePoints(count) {
const pts = [];
for (let i = 0; i < count; i++) {
const y = 1 - (i / (count - 1)) * 2;
const r = Math.sqrt(1 - y * y);
const theta = GOLDEN_ANGLE * i;
pts.push({ x: Math.cos(theta) * r, y, z: Math.sin(theta) * r });
}
return pts;
}
function rotatePoint(p, ry, rx) {
const x = p.x * Math.cos(ry) - p.z * Math.sin(ry);
const z = p.x * Math.sin(ry) + p.z * Math.cos(ry);
const y2 = p.y * Math.cos(rx) - z * Math.sin(rx);
const z2 = p.y * Math.sin(rx) + z * Math.cos(rx);
return { x, y: y2, z: z2 };
}
const points = computed(() => generateSpherePoints(props.icons.length));
function onMove(e) {
const rect = containerEl.value.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
tilt.targetY = (e.clientX - cx) * props.tiltSensitivity;
tilt.targetX = (e.clientY - cy) * props.tiltSensitivity;
}
function onLeave() {
tilt.targetX = 0;
tilt.targetY = 0;
}
function tick() {
if (!containerEl.value) return;
const size = containerEl.value.clientWidth / 2;
rot.y += props.rotateSpeed;
tilt.x += (tilt.targetX - tilt.x) * 0.05;
tilt.y += (tilt.targetY - tilt.y) * 0.05;
const totalRY = rot.y + tilt.y;
const totalRX = rot.x + tilt.x;
for (let i = 0; i < points.value.length; i++) {
const el = iconEls.value[i];
if (!el) continue;
const rotated = rotatePoint(points.value[i], totalRY, totalRX);
const scale = props.perspective / (props.perspective + rotated.z * size);
const px = rotated.x * size * scale;
const py = rotated.y * size * scale;
const opacity = Math.max(0.15, (rotated.z + 1) / 2);
const s = 0.6 + scale * 0.5;
el.style.transform = `translate(-50%, -50%) translate(${px}px, ${py}px) scale(${s})`;
el.style.opacity = String(opacity);
el.style.zIndex = String(Math.round(scale * 100));
}
animId = requestAnimationFrame(tick);
}
function setIconRef(el, i) {
if (el) iconEls.value[i] = el;
}
onMounted(() => {
tick();
});
onUnmounted(() => {
if (animId) cancelAnimationFrame(animId);
});
</script>
<template>
<div class="icon-cloud-demo">
<div
ref="containerEl"
class="cloud-container"
@mousemove="onMove"
@mouseleave="onLeave"
>
<div
v-for="(icon, i) in props.icons"
:key="i"
:ref="(el) => setIconRef(el, i)"
class="icon-item"
>
{{ icon }}
</div>
</div>
<div class="label-area">
<h1 class="cloud-title">Icon Cloud</h1>
<p class="cloud-subtitle">Technology icons orbiting in 3D space</p>
</div>
</div>
</template>
<style scoped>
.icon-cloud-demo {
width: 100vw;
height: 100vh;
background: #0a0a0a;
display: grid;
place-items: center;
position: relative;
font-family: system-ui, -apple-system, sans-serif;
}
.cloud-container {
position: relative;
width: 500px;
height: 500px;
max-width: 90vmin;
max-height: 90vmin;
}
.icon-item {
position: absolute;
left: 50%;
top: 50%;
font-size: 2rem;
line-height: 1;
pointer-events: none;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(4px);
will-change: transform, opacity;
}
.label-area {
position: absolute;
bottom: 10%;
left: 0;
right: 0;
text-align: center;
pointer-events: none;
z-index: 10;
}
.cloud-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #c4b5fd 0%, #8b5cf6 50%, #7c3aed 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 0.5rem;
}
.cloud-subtitle {
font-size: clamp(0.875rem, 2vw, 1.125rem);
color: rgba(148, 163, 184, 0.8);
margin: 0;
}
</style><script>
import { onMount, onDestroy } from "svelte";
export let icons = [
"\u269B\uFE0F",
"\u{1F310}",
"\u26A1",
"\u{1F4E6}",
"\u{1F680}",
"\u{1F3A8}",
"\u{1F527}",
"\u{1F4BB}",
"\u2728",
"\u{1F50D}",
"\u{1F4CA}",
"\u{1F512}",
"\u2601\uFE0F",
"\u{1F916}",
"\u{1F9E9}",
"\u{1F4A1}",
"\u{1F525}",
"\u{1F48E}",
"\u{1F3AF}",
"\u{1F30A}",
];
export let rotateSpeed = 0.004;
export let perspective = 500;
export let tiltSensitivity = 0.0003;
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
let containerEl;
let iconEls = [];
let animId;
let rot = { y: 0, x: 0 };
let tilt = { x: 0, y: 0, targetX: 0, targetY: 0 };
function generateSpherePoints(count) {
const pts = [];
for (let i = 0; i < count; i++) {
const y = 1 - (i / (count - 1)) * 2;
const r = Math.sqrt(1 - y * y);
const theta = GOLDEN_ANGLE * i;
pts.push({ x: Math.cos(theta) * r, y, z: Math.sin(theta) * r });
}
return pts;
}
function rotatePoint(p, ry, rx) {
const x = p.x * Math.cos(ry) - p.z * Math.sin(ry);
const z = p.x * Math.sin(ry) + p.z * Math.cos(ry);
const y2 = p.y * Math.cos(rx) - z * Math.sin(rx);
const z2 = p.y * Math.sin(rx) + z * Math.cos(rx);
return { x, y: y2, z: z2 };
}
$: points = generateSpherePoints(icons.length);
function onMove(e) {
const rect = containerEl.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
tilt.targetY = (e.clientX - cx) * tiltSensitivity;
tilt.targetX = (e.clientY - cy) * tiltSensitivity;
}
function onLeave() {
tilt.targetX = 0;
tilt.targetY = 0;
}
function tick() {
if (!containerEl) return;
const size = containerEl.clientWidth / 2;
rot.y += rotateSpeed;
tilt.x += (tilt.targetX - tilt.x) * 0.05;
tilt.y += (tilt.targetY - tilt.y) * 0.05;
const totalRY = rot.y + tilt.y;
const totalRX = rot.x + tilt.x;
for (let i = 0; i < points.length; i++) {
const el = iconEls[i];
if (!el) continue;
const rotated = rotatePoint(points[i], totalRY, totalRX);
const scale = perspective / (perspective + rotated.z * size);
const px = rotated.x * size * scale;
const py = rotated.y * size * scale;
const opacity = Math.max(0.15, (rotated.z + 1) / 2);
const s = 0.6 + scale * 0.5;
el.style.transform = `translate(-50%, -50%) translate(${px}px, ${py}px) scale(${s})`;
el.style.opacity = String(opacity);
el.style.zIndex = String(Math.round(scale * 100));
}
animId = requestAnimationFrame(tick);
}
onMount(() => {
tick();
});
onDestroy(() => {
if (animId) cancelAnimationFrame(animId);
});
</script>
<div class="icon-cloud-demo">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="cloud-container"
bind:this={containerEl}
on:mousemove={onMove}
on:mouseleave={onLeave}
>
{#each icons as icon, i}
<div class="icon-item" bind:this={iconEls[i]}>
{icon}
</div>
{/each}
</div>
<div class="label-area">
<h1 class="cloud-title">Icon Cloud</h1>
<p class="cloud-subtitle">Technology icons orbiting in 3D space</p>
</div>
</div>
<style>
.icon-cloud-demo {
width: 100vw;
height: 100vh;
background: #0a0a0a;
display: grid;
place-items: center;
position: relative;
font-family: system-ui, -apple-system, sans-serif;
}
.cloud-container {
position: relative;
width: 500px;
height: 500px;
max-width: 90vmin;
max-height: 90vmin;
}
.icon-item {
position: absolute;
left: 50%;
top: 50%;
font-size: 2rem;
line-height: 1;
pointer-events: none;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(4px);
will-change: transform, opacity;
}
.label-area {
position: absolute;
bottom: 10%;
left: 0;
right: 0;
text-align: center;
pointer-events: none;
z-index: 10;
}
.cloud-title {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #c4b5fd 0%, #8b5cf6 50%, #7c3aed 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 0.5rem;
}
.cloud-subtitle {
font-size: clamp(0.875rem, 2vw, 1.125rem);
color: rgba(148, 163, 184, 0.8);
margin: 0;
}
</style>Icon Cloud
A mesmerizing 3D icon cloud where technology icons (or any text/emoji) orbit in a spherical formation. Uses the Fibonacci spiral for even distribution and requestAnimationFrame for buttery-smooth rotation.
How it works
- Icons are positioned on a sphere surface using the Fibonacci spiral algorithm
- Each frame, all points rotate around the Y axis
- 3D coordinates are projected to 2D with perspective scaling
- Depth-based opacity and scale create a convincing 3D illusion
- Mouse proximity gently tilts the sphere for interactive feel
Customization
- Pass any array of icons (emoji, text, or image URLs)
- Adjust
RADIUS,ROTATE_SPEED, and perspective distance - Control tilt sensitivity with the mouse interaction multiplier
When to use it
- Technology stack showcases
- Skills / tools sections on portfolios
- Animated brand partner displays
- Feature highlights