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>Glowing Dots Loader</title>
7<style>
8 :root{
9 --bg-edge:#2b2d31;
10 --bg-mid:#14161a;
11 --bg-core:#050607;
12
13 --pink:#ff0aa8;
14 --violet:#9a4dff;
15 --cyan:#18b8ff;
16
17 --dot-size:min(3.2vmin, 26px);
18 --gap:min(7.8vmin, 80px);
19 --cycle:2.4s;
20 }
21
22 *{box-sizing:border-box}
23 html,body{
24 margin:0;
25 width:100%;
26 height:100%;
27 overflow:hidden;
28 background:
29 radial-gradient(circle at 50% 50%,
30 var(--bg-core) 0%,
31 #07090b 12%,
32 #0d1014 28%,
33 #171a1f 52%,
34 #23262b 78%,
35 var(--bg-edge) 100%);
36 }
37
38 body{
39 display:grid;
40 place-items:center;
41 font-family:system-ui,sans-serif;
42 }
43
44 .stage{
45 position:relative;
46 width:100vw;
47 height:100vh;
48 filter: blur(.15px);
49 }
50
51 .loader{
52 position:absolute;
53 left:50%;
54 top:50%;
55 transform:translate(-50%,-50%);
56 width:calc(var(--gap) * 2 + var(--dot-size));
57 height:calc(var(--dot-size) * 2.2);
58 }
59
60 .dot{
61 position:absolute;
62 top:50%;
63 width:var(--dot-size);
64 height:var(--dot-size);
65 border-radius:50%;
66 transform:translate(-50%,-50%) scale(1);
67 opacity:.18;
68 will-change:opacity, transform, filter;
69 }
70
71 .dot::before{
72 content:"";
73 position:absolute;
74 inset:0;
75 border-radius:50%;
76 background:currentColor;
77 box-shadow:
78 0 0 8px currentColor,
79 0 0 16px currentColor,
80 0 0 28px currentColor;
81 }
82
83 .dot::after{
84 content:"";
85 position:absolute;
86 inset:-55%;
87 border-radius:50%;
88 background:radial-gradient(circle, currentColor 0%, color-mix(in srgb, currentColor 55%, transparent) 28%, transparent 72%);
89 filter:blur(10px);
90 opacity:.55;
91 }
92
93 .pink{ left:0%; color:var(--pink); animation:pulse var(--cycle) infinite ease-in-out; }
94 .violet{ left:50%; color:var(--violet); animation:pulse var(--cycle) infinite ease-in-out .28s; }
95 .cyan{ left:100%; color:var(--cyan); animation:pulse var(--cycle) infinite ease-in-out .56s; }
96
97 @keyframes pulse{
98 0%, 18%{
99 opacity:.16;
100 transform:translate(-50%,-50%) scale(.92);
101 filter:brightness(.65);
102 }
103 28%, 42%{
104 opacity:1;
105 transform:translate(-50%,-50%) scale(1.08);
106 filter:brightness(1.25);
107 }
108 52%{
109 opacity:.72;
110 transform:translate(-50%,-50%) scale(1);
111 filter:brightness(1);
112 }
113 70%, 100%{
114 opacity:.16;
115 transform:translate(-50%,-50%) scale(.92);
116 filter:brightness(.65);
117 }
118 }
119
120 /* subtle center vignette softness */
121 .stage::after{
122 content:"";
123 position:absolute;
124 inset:0;
125 background:
126 radial-gradient(circle at 50% 50%,
127 transparent 0 8%,
128 rgba(0,0,0,.08) 20%,
129 rgba(0,0,0,.18) 42%,
130 rgba(0,0,0,.28) 100%);
131 pointer-events:none;
132 mix-blend-mode:multiply;
133 }
134</style>
135</head>
136<body>
137 <div class="stage">
138 <div class="loader" aria-label="loading animation">
139 <span class="dot pink"></span>
140 <span class="dot violet"></span>
141 <span class="dot cyan"></span>
142 </div>
143 </div>
144
145<script>
146 // Keep the animation phase aligned on load for a clean first frame.
147 document.documentElement.animate(
148 [{ opacity: 1 }, { opacity: 1 }],
149 { duration: 1, fill: "forwards" }
150 );
151</script>
152</body>
153</html>