← back to Animated Dot Loaderscompare with ground truth →
model output
GPT-5.4
Animated Dot Loaders
A 0.93T 0.21
1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8" />
5<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6<title>Dot Merge Loader</title>
7<style>
8 :root{
9 --bg:#17141f;
10 --bg2:#241238;
11 --dot:#a79cab;
12 --active:#f2eef4;
13 --size:20px;
14 --gap:9px;
15 --dur:1.8s;
16 }
17
18 *{box-sizing:border-box}
19 html,body{
20 margin:0;
21 width:100%;
22 height:100%;
23 overflow:hidden;
24 background:
25 radial-gradient(circle at 50% 50%, rgba(58,24,92,.42) 0 16%, rgba(36,18,56,.22) 28%, rgba(23,20,31,0) 52%),
26 radial-gradient(circle at 50% 50%, #22152f 0%, #191622 55%, #14131a 100%);
27 font-family:system-ui,sans-serif;
28 }
29
30 .stage{
31 width:100%;
32 height:100%;
33 display:grid;
34 place-items:center;
35 }
36
37 .loader{
38 position:relative;
39 width:calc(var(--size) * 5 + var(--gap) * 4);
40 height:var(--size);
41 }
42
43 .dot{
44 position:absolute;
45 top:0;
46 width:var(--size);
47 height:var(--size);
48 border-radius:50%;
49 background:var(--dot);
50 box-shadow:
51 inset 0 0 0 1px rgba(255,255,255,.08),
52 0 0 8px rgba(255,255,255,.04);
53 animation: move var(--dur) cubic-bezier(.55,.08,.45,.92) infinite;
54 will-change: transform, background-color, opacity;
55 }
56
57 .dot::after{
58 content:"";
59 position:absolute;
60 inset:0;
61 border-radius:50%;
62 background:radial-gradient(circle at 38% 35%, rgba(255,255,255,.55), rgba(255,255,255,0) 45%);
63 opacity:.35;
64 pointer-events:none;
65 }
66
67 .d1{ animation-delay:0s; }
68 .d2{ animation-delay:calc(var(--dur) * -0.08); }
69 .d3{ animation-delay:calc(var(--dur) * -0.16); }
70 .d4{ animation-delay:calc(var(--dur) * -0.24); }
71 .d5{ animation-delay:calc(var(--dur) * -0.32); }
72
73 @keyframes move{
74 /* 5 dots spread */
75 0%, 14%{
76 transform:translateX(calc((var(--size) + var(--gap)) * var(--i)));
77 background:var(--dot);
78 opacity:1;
79 }
80
81 /* collapse toward center */
82 28%{
83 transform:translateX(calc((var(--size) + var(--gap)) * (2 + (var(--i) - 2) * .55)));
84 background:var(--dot);
85 opacity:1;
86 }
87
88 42%{
89 transform:translateX(calc((var(--size) + var(--gap)) * (2 + (var(--i) - 2) * .18)));
90 background:color-mix(in srgb, var(--dot) 70%, var(--active));
91 opacity:calc(1 - min(abs(var(--i) - 2), 1) * .08);
92 }
93
94 /* merge to single center dot */
95 50%, 58%{
96 transform:translateX(calc((var(--size) + var(--gap)) * 2));
97 background:var(--active);
98 opacity:calc(1 - min(abs(var(--i) - 2), 1) * .92 - min(abs(var(--i) - 2), 2) * .04);
99 }
100
101 /* expand to 3 dots */
102 70%{
103 transform:translateX(calc((var(--size) + var(--gap)) * (2 + (var(--i) - 2) * .5)));
104 background:color-mix(in srgb, var(--dot) 55%, var(--active));
105 opacity:calc(1 - max(0, abs(var(--i) - 2) - 1) * .95);
106 }
107
108 /* settle to 3 visible dots */
109 82%{
110 transform:translateX(calc((var(--size) + var(--gap)) * (2 + (var(--i) - 2) * .95)));
111 background:var(--dot);
112 opacity:calc(1 - max(0, abs(var(--i) - 2) - 1) * .98);
113 }
114
115 /* return to 5 dots */
116 100%{
117 transform:translateX(calc((var(--size) + var(--gap)) * var(--i)));
118 background:var(--dot);
119 opacity:1;
120 }
121 }
122
123 /* brighten center-most visible dot during the 3-dot phase */
124 .d3{
125 filter:brightness(1.08);
126 }
127</style>
128</head>
129<body>
130 <div class="stage">
131 <div class="loader" aria-label="Loading" role="img">
132 <span class="dot d1" style="--i:0"></span>
133 <span class="dot d2" style="--i:1"></span>
134 <span class="dot d3" style="--i:2"></span>
135 <span class="dot d4" style="--i:3"></span>
136 <span class="dot d5" style="--i:4"></span>
137 </div>
138 </div>
139</body>
140</html>