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>Three Dots Loader</title>
7<style>
8 :root{
9 --bg:#ef4b3a;
10 --dot:#f7f2ef;
11 --ghost:rgba(247,242,239,.38);
12 --size:12px;
13 --gap:13px;
14 --dur:1.8s;
15 }
16
17 html,body{
18 margin:0;
19 width:100%;
20 height:100%;
21 overflow:hidden;
22 background:var(--bg);
23 }
24
25 body{
26 position:relative;
27 font-family:sans-serif;
28 }
29
30 .loader{
31 position:absolute;
32 left:50%;
33 top:8.2%;
34 width:calc(var(--size) * 3 + var(--gap) * 2);
35 height:var(--size);
36 transform:translateX(-50%);
37 }
38
39 .dot{
40 position:absolute;
41 top:0;
42 width:var(--size);
43 height:var(--size);
44 border-radius:50%;
45 background:var(--dot);
46 animation:dotCycle var(--dur) cubic-bezier(.45,0,.55,1) infinite;
47 will-change:transform, opacity;
48 }
49
50 .dot::after{
51 content:"";
52 position:absolute;
53 inset:0;
54 border-radius:50%;
55 background:var(--ghost);
56 transform:translateX(0) scale(1);
57 opacity:0;
58 animation:trailCycle var(--dur) cubic-bezier(.45,0,.55,1) infinite;
59 will-change:transform, opacity;
60 }
61
62 .d1{ left:0; animation-delay:0s; }
63 .d2{ left:calc(var(--size) + var(--gap)); animation-delay:0s; }
64 .d3{ left:calc((var(--size) + var(--gap)) * 2); animation-delay:0s; }
65
66 .d1::after{ animation-delay:0s; }
67 .d2::after{ animation-delay:0s; }
68 .d3::after{ animation-delay:0s; }
69
70 @keyframes dotCycle{
71 0%, 18% { transform:translateX(0); opacity:1; }
72 32% { transform:translateX(calc(var(--size) + var(--gap))); opacity:1; }
73 50% { transform:translateX(calc(var(--size) + var(--gap))); opacity:1; }
74 64% { transform:translateX(0); opacity:1; }
75 100% { transform:translateX(0); opacity:1; }
76 }
77
78 /* phase offsets create the collapse to center dot, hold, then expand */
79 .d1{ animation-name:leftDot; }
80 .d2{ animation-name:centerDot; }
81 .d3{ animation-name:rightDot; }
82
83 @keyframes leftDot{
84 0%,20% { transform:translateX(0); }
85 36%,64% { transform:translateX(calc(var(--size) + var(--gap))); }
86 80%,100% { transform:translateX(0); }
87 }
88
89 @keyframes centerDot{
90 0%,100% { transform:translateX(0); }
91 }
92
93 @keyframes rightDot{
94 0%,20% { transform:translateX(0); }
95 36%,64% { transform:translateX(calc(-1 * (var(--size) + var(--gap)))); }
96 80%,100% { transform:translateX(0); }
97 }
98
99 /* faint side remnants visible during merge/split */
100 .d1::after{ animation-name:leftTrail; }
101 .d2::after{ animation-name:centerTrail; }
102 .d3::after{ animation-name:rightTrail; }
103
104 @keyframes leftTrail{
105 0%,22%,78%,100% { opacity:0; transform:translateX(0) scale(1); }
106 30% { opacity:1; transform:translateX(6px) scale(1.05); }
107 40%,60% { opacity:.95; transform:translateX(10px) scale(1.05); }
108 70% { opacity:1; transform:translateX(6px) scale(1.05); }
109 }
110
111 @keyframes centerTrail{
112 0%,100% { opacity:0; transform:translateX(0) scale(1); }
113 34% { opacity:.35; transform:translateX(-2px) scale(1.08); }
114 50% { opacity:.18; transform:translateX(0) scale(1.12); }
115 66% { opacity:.35; transform:translateX(2px) scale(1.08); }
116 }
117
118 @keyframes rightTrail{
119 0%,22%,78%,100% { opacity:0; transform:translateX(0) scale(1); }
120 30% { opacity:1; transform:translateX(-6px) scale(1.05); }
121 40%,60% { opacity:.95; transform:translateX(-10px) scale(1.05); }
122 70% { opacity:1; transform:translateX(-6px) scale(1.05); }
123 }
124</style>
125</head>
126<body>
127 <div class="loader" aria-label="loading">
128 <span class="dot d1"></span>
129 <span class="dot d2"></span>
130 <span class="dot d3"></span>
131 </div>
132</body>
133</html>