UI Components Easy
Spinner
Multiple spinner and loading animations — circular, dots, bars, and pulse — in various sizes and colors with pure CSS keyframes.
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: Inter, system-ui, sans-serif;
background: #0a0a0a;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 520px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.demo-section {
margin-bottom: 2rem;
}
.demo-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.demo-row {
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ── Base ── */
.spinner {
--spinner-color: #38bdf8;
--spinner-size: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.spinner--sm {
--spinner-size: 16px;
}
.spinner--lg {
--spinner-size: 36px;
}
/* ── Circle ── */
.spinner--circle {
width: var(--spinner-size);
height: var(--spinner-size);
border: 2.5px solid rgba(255, 255, 255, 0.08);
border-top-color: var(--spinner-color);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
.spinner--sm.spinner--circle {
border-width: 2px;
}
.spinner--lg.spinner--circle {
border-width: 3px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ── Dots ── */
.spinner--dots {
gap: calc(var(--spinner-size) * 0.25);
}
.spinner--dots .dot {
width: calc(var(--spinner-size) * 0.35);
height: calc(var(--spinner-size) * 0.35);
background: var(--spinner-color);
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite both;
}
.spinner--dots .dot:nth-child(1) {
animation-delay: -0.32s;
}
.spinner--dots .dot:nth-child(2) {
animation-delay: -0.16s;
}
.spinner--dots .dot:nth-child(3) {
animation-delay: 0s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0.4);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* ── Bars ── */
.spinner--bars {
gap: calc(var(--spinner-size) * 0.1);
height: var(--spinner-size);
}
.spinner--bars .bar {
width: calc(var(--spinner-size) * 0.15);
height: 100%;
background: var(--spinner-color);
border-radius: 2px;
animation: bars 1.2s ease-in-out infinite;
}
.spinner--bars .bar:nth-child(1) {
animation-delay: -0.36s;
}
.spinner--bars .bar:nth-child(2) {
animation-delay: -0.24s;
}
.spinner--bars .bar:nth-child(3) {
animation-delay: -0.12s;
}
.spinner--bars .bar:nth-child(4) {
animation-delay: 0s;
}
@keyframes bars {
0%,
40%,
100% {
transform: scaleY(0.4);
opacity: 0.4;
}
20% {
transform: scaleY(1);
opacity: 1;
}
}
/* ── Pulse ── */
.spinner--pulse {
width: var(--spinner-size);
height: var(--spinner-size);
position: relative;
}
.spinner--pulse::before,
.spinner--pulse::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--spinner-color);
}
.spinner--pulse::before {
animation: pulse-ring 1.5s ease-out infinite;
}
.spinner--pulse::after {
animation: pulse-dot 1.5s ease-out infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(0.5);
opacity: 0.6;
}
100% {
transform: scale(1.8);
opacity: 0;
}
}
@keyframes pulse-dot {
0%,
100% {
transform: scale(0.5);
opacity: 1;
}
50% {
transform: scale(0.7);
opacity: 0.8;
}
}/* Spinner is pure CSS — no JavaScript required. */<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Spinner</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Spinner</h1>
<p class="demo-sub">Multiple loading animation styles and sizes.</p>
<!-- Circle Spinner -->
<div class="demo-section">
<span class="demo-label">Circle</span>
<div class="demo-row">
<div class="spinner spinner--circle spinner--sm" role="status">
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--circle" role="status">
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--circle spinner--lg" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
<!-- Dots Spinner -->
<div class="demo-section">
<span class="demo-label">Dots</span>
<div class="demo-row">
<div class="spinner spinner--dots spinner--sm" role="status">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--dots" role="status">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--dots spinner--lg" role="status">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
<!-- Bars Spinner -->
<div class="demo-section">
<span class="demo-label">Bars</span>
<div class="demo-row">
<div class="spinner spinner--bars spinner--sm" role="status">
<span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span>
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--bars" role="status">
<span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span>
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--bars spinner--lg" role="status">
<span class="bar"></span><span class="bar"></span><span class="bar"></span><span class="bar"></span>
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
<!-- Pulse Spinner -->
<div class="demo-section">
<span class="demo-label">Pulse</span>
<div class="demo-row">
<div class="spinner spinner--pulse spinner--sm" role="status">
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--pulse" role="status">
<span class="sr-only">Loading...</span>
</div>
<div class="spinner spinner--pulse spinner--lg" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
<!-- Color variants -->
<div class="demo-section">
<span class="demo-label">Colors</span>
<div class="demo-row">
<div class="spinner spinner--circle" style="--spinner-color: #38bdf8" role="status"><span class="sr-only">Loading...</span></div>
<div class="spinner spinner--circle" style="--spinner-color: #22c55e" role="status"><span class="sr-only">Loading...</span></div>
<div class="spinner spinner--circle" style="--spinner-color: #f59e0b" role="status"><span class="sr-only">Loading...</span></div>
<div class="spinner spinner--circle" style="--spinner-color: #ef4444" role="status"><span class="sr-only">Loading...</span></div>
<div class="spinner spinner--circle" style="--spinner-color: #a855f7" role="status"><span class="sr-only">Loading...</span></div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { CSSProperties } from "react";
type SpinnerVariant = "circle" | "dots" | "bars" | "pulse";
type SpinnerSize = "sm" | "md" | "lg";
interface SpinnerProps {
variant?: SpinnerVariant;
size?: SpinnerSize;
color?: string;
}
const sizes: Record<SpinnerSize, number> = { sm: 16, md: 24, lg: 36 };
export function Spinner({ variant = "circle", size = "md", color = "#38bdf8" }: SpinnerProps) {
const s = sizes[size];
if (variant === "circle") {
const bw = size === "sm" ? 2 : size === "lg" ? 3 : 2.5;
return (
<div
role="status"
style={{
width: s,
height: s,
border: `${bw}px solid rgba(255,255,255,0.08)`,
borderTopColor: color,
borderRadius: "50%",
animation: "spinner-spin 0.7s linear infinite",
}}
>
<span style={srOnly}>Loading...</span>
<style>{`@keyframes spinner-spin { to { transform: rotate(360deg); } }`}</style>
</div>
);
}
if (variant === "dots") {
const dotSize = s * 0.35;
const gap = s * 0.25;
return (
<div role="status" style={{ display: "inline-flex", alignItems: "center", gap }}>
{["-0.32s", "-0.16s", "0s"].map((delay, i) => (
<span
key={i}
style={{
width: dotSize,
height: dotSize,
background: color,
borderRadius: "50%",
animation: "spinner-bounce 1.4s ease-in-out infinite both",
animationDelay: delay,
}}
/>
))}
<span style={srOnly}>Loading...</span>
<style>{`@keyframes spinner-bounce { 0%, 80%, 100% { transform: scale(0.4); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }`}</style>
</div>
);
}
if (variant === "bars") {
const barW = s * 0.15;
const gap = s * 0.1;
return (
<div role="status" style={{ display: "inline-flex", alignItems: "center", height: s, gap }}>
{["-0.36s", "-0.24s", "-0.12s", "0s"].map((delay, i) => (
<span
key={i}
style={{
width: barW,
height: "100%",
background: color,
borderRadius: 2,
animation: "spinner-bars 1.2s ease-in-out infinite",
animationDelay: delay,
}}
/>
))}
<span style={srOnly}>Loading...</span>
<style>{`@keyframes spinner-bars { 0%, 40%, 100% { transform: scaleY(0.4); opacity: 0.4; } 20% { transform: scaleY(1); opacity: 1; } }`}</style>
</div>
);
}
/* pulse */
return (
<div role="status" style={{ position: "relative", width: s, height: s }}>
<span
style={{
position: "absolute",
inset: 0,
borderRadius: "50%",
background: color,
animation: "spinner-pulse-ring 1.5s ease-out infinite",
}}
/>
<span
style={{
position: "absolute",
inset: 0,
borderRadius: "50%",
background: color,
animation: "spinner-pulse-dot 1.5s ease-out infinite",
}}
/>
<span style={srOnly}>Loading...</span>
<style>{`
@keyframes spinner-pulse-ring { 0% { transform: scale(0.5); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } }
@keyframes spinner-pulse-dot { 0%, 100% { transform: scale(0.5); opacity: 1; } 50% { transform: scale(0.7); opacity: 0.8; } }
`}</style>
</div>
);
}
const srOnly: CSSProperties = {
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clip: "rect(0,0,0,0)",
whiteSpace: "nowrap",
border: 0,
};
/* Demo */
export default function SpinnerDemo() {
const variants: SpinnerVariant[] = ["circle", "dots", "bars", "pulse"];
const sizeList: SpinnerSize[] = ["sm", "md", "lg"];
const colors = ["#38bdf8", "#22c55e", "#f59e0b", "#ef4444", "#a855f7"];
return (
<div
style={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#0a0a0a",
fontFamily: "Inter, system-ui, sans-serif",
color: "#f2f6ff",
padding: "2rem",
}}
>
<div style={{ width: "100%", maxWidth: 520 }}>
<h1 style={{ fontSize: "1.5rem", fontWeight: 800, marginBottom: "0.375rem" }}>Spinner</h1>
<p style={{ color: "#475569", fontSize: "0.875rem", marginBottom: "2rem" }}>
Multiple loading animation styles and sizes.
</p>
{variants.map((v) => (
<Section key={v} label={v}>
<div style={{ display: "flex", alignItems: "center", gap: "1.5rem", flexWrap: "wrap" }}>
{sizeList.map((sz) => (
<Spinner key={sz} variant={v} size={sz} />
))}
</div>
</Section>
))}
<Section label="Colors">
<div style={{ display: "flex", alignItems: "center", gap: "1.5rem", flexWrap: "wrap" }}>
{colors.map((c) => (
<Spinner key={c} variant="circle" color={c} />
))}
</div>
</Section>
</div>
</div>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ marginBottom: "2rem" }}>
<span
style={{
display: "block",
fontSize: "0.75rem",
fontWeight: 600,
color: "#94a3b8",
textTransform: "uppercase",
letterSpacing: "0.05em",
marginBottom: "0.75rem",
}}
>
{label}
</span>
{children}
</div>
);
}<script setup>
const sizes = { sm: 16, md: 24, lg: 36 };
const variants = ["circle", "dots", "bars", "pulse"];
const sizeList = ["sm", "md", "lg"];
const colorList = ["#38bdf8", "#22c55e", "#f59e0b", "#ef4444", "#a855f7"];
function getSize(size) {
return sizes[size] || sizes.md;
}
function borderWidth(size) {
return size === "sm" ? 2 : size === "lg" ? 3 : 2.5;
}
</script>
<template>
<div class="demo">
<div class="container">
<h1 class="title">Spinner</h1>
<p class="desc">Multiple loading animation styles and sizes.</p>
<div v-for="variant in variants" :key="variant" class="section">
<span class="section-label">{{ variant }}</span>
<div class="row">
<template v-for="sz in sizeList" :key="sz">
<!-- Circle -->
<div v-if="variant === 'circle'" role="status"
:style="{
width: getSize(sz) + 'px', height: getSize(sz) + 'px',
border: borderWidth(sz) + 'px solid rgba(255,255,255,0.08)',
borderTopColor: '#38bdf8', borderRadius: '50%',
animation: 'spinner-spin 0.7s linear infinite',
}">
<span class="sr-only">Loading...</span>
</div>
<!-- Dots -->
<div v-else-if="variant === 'dots'" role="status"
:style="{ display: 'inline-flex', alignItems: 'center', gap: getSize(sz) * 0.25 + 'px' }">
<span v-for="(delay, i) in ['-0.32s', '-0.16s', '0s']" :key="i"
:style="{
width: getSize(sz) * 0.35 + 'px', height: getSize(sz) * 0.35 + 'px',
background: '#38bdf8', borderRadius: '50%',
animation: 'spinner-bounce 1.4s ease-in-out infinite both',
animationDelay: delay,
}"></span>
<span class="sr-only">Loading...</span>
</div>
<!-- Bars -->
<div v-else-if="variant === 'bars'" role="status"
:style="{ display: 'inline-flex', alignItems: 'center', height: getSize(sz) + 'px', gap: getSize(sz) * 0.1 + 'px' }">
<span v-for="(delay, i) in ['-0.36s', '-0.24s', '-0.12s', '0s']" :key="i"
:style="{
width: getSize(sz) * 0.15 + 'px', height: '100%',
background: '#38bdf8', borderRadius: '2px',
animation: 'spinner-bars 1.2s ease-in-out infinite',
animationDelay: delay,
}"></span>
<span class="sr-only">Loading...</span>
</div>
<!-- Pulse -->
<div v-else role="status"
:style="{ position: 'relative', width: getSize(sz) + 'px', height: getSize(sz) + 'px' }">
<span :style="{ position: 'absolute', inset: '0', borderRadius: '50%', background: '#38bdf8', animation: 'spinner-pulse-ring 1.5s ease-out infinite' }"></span>
<span :style="{ position: 'absolute', inset: '0', borderRadius: '50%', background: '#38bdf8', animation: 'spinner-pulse-dot 1.5s ease-out infinite' }"></span>
<span class="sr-only">Loading...</span>
</div>
</template>
</div>
</div>
<div class="section">
<span class="section-label">Colors</span>
<div class="row">
<div v-for="c in colorList" :key="c" role="status"
:style="{
width: '24px', height: '24px',
border: '2.5px solid rgba(255,255,255,0.08)',
borderTopColor: c, borderRadius: '50%',
animation: 'spinner-spin 0.7s linear infinite',
}">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style>
@keyframes spinner-spin { to { transform: rotate(360deg); } }
@keyframes spinner-bounce { 0%, 80%, 100% { transform: scale(0.4); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
@keyframes spinner-bars { 0%, 40%, 100% { transform: scaleY(0.4); opacity: 0.4; } 20% { transform: scaleY(1); opacity: 1; } }
@keyframes spinner-pulse-ring { 0% { transform: scale(0.5); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } }
@keyframes spinner-pulse-dot { 0%, 100% { transform: scale(0.5); opacity: 1; } 50% { transform: scale(0.7); opacity: 0.8; } }
.demo {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
font-family: Inter, system-ui, sans-serif;
color: #f2f6ff;
padding: 2rem;
}
.container { width: 100%; max-width: 520px; }
.title { font-size: 1.5rem; font-weight: 800; margin-bottom: 0.375rem; }
.desc { color: #475569; font-size: 0.875rem; margin-bottom: 2rem; }
.section { margin-bottom: 2rem; }
.section-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.row { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
</style><script>
const sizes = { sm: 16, md: 24, lg: 36 };
const variants = ["circle", "dots", "bars", "pulse"];
const sizeList = ["sm", "md", "lg"];
const colors = ["#38bdf8", "#22c55e", "#f59e0b", "#ef4444", "#a855f7"];
function getSize(size) {
return sizes[size] || sizes.md;
}
function borderWidth(size) {
return size === "sm" ? 2 : size === "lg" ? 3 : 2.5;
}
</script>
<div class="demo">
<div class="container">
<h1 class="title">Spinner</h1>
<p class="desc">Multiple loading animation styles and sizes.</p>
{#each variants as variant}
<div class="section">
<span class="section-label">{variant}</span>
<div class="row">
{#each sizeList as sz}
{@const s = getSize(sz)}
{#if variant === 'circle'}
<div role="status" style="width: {s}px; height: {s}px; border: {borderWidth(sz)}px solid rgba(255,255,255,0.08); border-top-color: #38bdf8; border-radius: 50%; animation: spinner-spin 0.7s linear infinite;">
<span class="sr-only">Loading...</span>
</div>
{:else if variant === 'dots'}
{@const dotSize = s * 0.35}
{@const gap = s * 0.25}
<div role="status" style="display: inline-flex; align-items: center; gap: {gap}px;">
{#each ['-0.32s', '-0.16s', '0s'] as delay}
<span style="width: {dotSize}px; height: {dotSize}px; background: #38bdf8; border-radius: 50%; animation: spinner-bounce 1.4s ease-in-out infinite both; animation-delay: {delay};"></span>
{/each}
<span class="sr-only">Loading...</span>
</div>
{:else if variant === 'bars'}
{@const barW = s * 0.15}
{@const gap = s * 0.1}
<div role="status" style="display: inline-flex; align-items: center; height: {s}px; gap: {gap}px;">
{#each ['-0.36s', '-0.24s', '-0.12s', '0s'] as delay}
<span style="width: {barW}px; height: 100%; background: #38bdf8; border-radius: 2px; animation: spinner-bars 1.2s ease-in-out infinite; animation-delay: {delay};"></span>
{/each}
<span class="sr-only">Loading...</span>
</div>
{:else}
<div role="status" style="position: relative; width: {s}px; height: {s}px;">
<span style="position: absolute; inset: 0; border-radius: 50%; background: #38bdf8; animation: spinner-pulse-ring 1.5s ease-out infinite;"></span>
<span style="position: absolute; inset: 0; border-radius: 50%; background: #38bdf8; animation: spinner-pulse-dot 1.5s ease-out infinite;"></span>
<span class="sr-only">Loading...</span>
</div>
{/if}
{/each}
</div>
</div>
{/each}
<div class="section">
<span class="section-label">Colors</span>
<div class="row">
{#each colors as c}
<div role="status" style="width: 24px; height: 24px; border: 2.5px solid rgba(255,255,255,0.08); border-top-color: {c}; border-radius: 50%; animation: spinner-spin 0.7s linear infinite;">
<span class="sr-only">Loading...</span>
</div>
{/each}
</div>
</div>
</div>
</div>
<style>
@keyframes -global-spinner-spin { to { transform: rotate(360deg); } }
@keyframes -global-spinner-bounce { 0%, 80%, 100% { transform: scale(0.4); opacity: 0.4; } 40% { transform: scale(1); opacity: 1; } }
@keyframes -global-spinner-bars { 0%, 40%, 100% { transform: scaleY(0.4); opacity: 0.4; } 20% { transform: scaleY(1); opacity: 1; } }
@keyframes -global-spinner-pulse-ring { 0% { transform: scale(0.5); opacity: 0.6; } 100% { transform: scale(1.8); opacity: 0; } }
@keyframes -global-spinner-pulse-dot { 0%, 100% { transform: scale(0.5); opacity: 1; } 50% { transform: scale(0.7); opacity: 0.8; } }
.demo {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
font-family: Inter, system-ui, sans-serif;
color: #f2f6ff;
padding: 2rem;
}
.container { width: 100%; max-width: 520px; }
.title { font-size: 1.5rem; font-weight: 800; margin-bottom: 0.375rem; }
.desc { color: #475569; font-size: 0.875rem; margin-bottom: 2rem; }
.section { margin-bottom: 2rem; }
.section-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.row { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
</style>Spinner
Multiple loading animation styles — circle, dots, bars, and pulse — implemented with pure CSS keyframes.
Variants
| Variant | Animation |
|---|---|
circle | Rotating ring with gradient tail |
dots | Three bouncing dots |
bars | Oscillating vertical bars |
pulse | Pulsing circle |
Features
- Multiple animation variants
- Three size options (sm, md, lg)
- Customizable color via CSS variable
- Accessible
role="status"with sr-only label