Dual Ring Loader
A modern loader with two rotating rings moving in opposite directions. It uses pure HTML and CSS, with no JavaScript required.
<div class="dual-ring-loader" aria-label="Loading">
<span></span>
<span></span>
</div>
.dual-ring-loader{
position:relative;
width:72px;
height:72px;
}
.dual-ring-loader span{
position:absolute;
border-radius:50%;
}
.dual-ring-loader span:first-child{
inset:0;
border:5px solid transparent;
border-top-color:#22d3ee;
border-right-color:#22d3ee;
animation:dualRingOuter 1s linear infinite;
}
.dual-ring-loader span:last-child{
inset:12px;
border:5px solid transparent;
border-bottom-color:#8b5cf6;
border-left-color:#8b5cf6;
animation:dualRingInner .8s linear infinite;
}
@keyframes dualRingOuter{
to{
transform:rotate(360deg);
}
}
@keyframes dualRingInner{
to{
transform:rotate(-360deg);
}
}
How This Dual Ring Loader Works
The Dual Ring Loader is a CSS-only loading indicator built from one parent <div> and two nested <span> elements. Each span becomes a circular ring, and the two rings rotate in opposite directions at slightly different speeds. There is no JavaScript in this component, so the animation starts automatically when the CSS is applied.
The outer .dual-ring-loader element is 72px wide and 72px high. It uses position: relative, which creates the positioning reference for both rings inside it. The parent itself does not have a visible background or border; its main job is to define the loader’s overall dimensions and hold the two absolutely positioned circles.
Both spans use position: absolute and border-radius: 50%. The 50% radius turns each square span into a circular shape. Their final sizes are controlled with the inset property rather than separate width, height, top, and left values.
The first span forms the outer ring. It uses inset: 0, so it fills the complete 72px container. A 5px solid transparent border is applied around the circle, but only the top and right border colors are changed to cyan #22d3ee. This leaves half of the circular border visible while the other half remains transparent.
The outer ring runs the dualRingOuter animation. It lasts 1s, uses a linear timing function, and repeats infinitely. Its keyframe rotates the ring to 360deg, producing one complete clockwise rotation during each cycle. Because the animation is linear, the rotational speed stays constant rather than accelerating or slowing down.
The second span forms the smaller inner ring. Its inset: 12px value pulls every edge 12px inward from the parent container. This creates a smaller circle centered inside the outer ring without requiring manual width and height calculations.
Like the outer circle, the inner ring uses a 5px solid transparent border. However, its visible sections are the bottom and left borders, which use purple #8b5cf6. This places the colored part of the inner ring on the opposite side from the cyan outer ring.
The inner ring uses the dualRingInner animation, which lasts .8s. Its transform ends at rotate(-360deg). The negative rotation value makes this ring rotate counterclockwise, opposite to the outer ring. The shorter .8s duration also means it completes each rotation faster than the outer ring.
The combination of opposite directions and different animation durations gives the loader more visual movement than two rings spinning together at the same speed. The rings periodically shift their relative positions as one completes its cycles faster than the other.
Both animations use CSS transform: rotate(), which handles the circular motion. For more information about rotating elements with this property, see the MDN CSS transform documentation.
The HTML also includes aria-label="Loading" on the parent element. This provides a simple accessible description for the visual loading state even though the component does not display any text.
Customizing the Dual Ring Loader
The loader can be adjusted through a small set of dimensions, border values, colors, and animation timings.
- Change the
72pxwidth and height of.dual-ring-loaderto resize the complete component. - Adjust the
5pxborder widths to make both circular rings thinner or thicker. - Replace
#22d3eeto change the cyan color of the outer ring. - Replace
#8b5cf6to change the purple color of the inner ring. - Modify
inset: 12pxon the second span to increase or reduce the space between the two rings. - Change the outer animation duration from
1sto make its clockwise rotation faster or slower. - Adjust the inner
.8sduration to change how quickly it moves relative to the outer ring. - Reverse the positive or negative rotation values if you want to swap the rings’ rotation directions.
Where to Use the Dual Ring Loader
This loader is useful for interfaces where the completion time is unknown and a continuous activity indicator is more appropriate than a percentage.
- Dashboard sections while data is being fetched.
- Form submission states during server processing.
- Modal windows waiting for asynchronous content.
- Full-page or section-level loading overlays.
- Cards and widgets that load independently.
- Buttons or compact interface areas that need an indefinite loading state.
For another circular CSS-only design with a multicolor effect, the Gradient Spinner Loader is a related CodeRipple component worth considering. The Dual Ring Loader works particularly well when you want a clear sense of continuous motion while keeping the markup and CSS relatively small.
Frequently Asked Questions
Does the Dual Ring Loader use JavaScript?
No. The component contains no JavaScript. Both rings are created and animated entirely with HTML and CSS.
Why do the two rings rotate in opposite directions?
The outer ring rotates to 360deg, while the inner ring rotates to -360deg. These positive and negative rotation values make the circles move clockwise and counterclockwise.
Why is the inner ring smaller than the outer ring?
The second span uses inset: 12px, which moves all four of its edges inward by 12px. This creates a smaller centered circle inside the 72px outer container.
Do both rings rotate at the same speed?
No. The outer ring completes a rotation every 1s, while the inner ring completes one every .8s. The inner ring therefore moves faster.
The Dual Ring Loader combines two partial circular borders, opposite rotation directions, and different animation speeds to create a compact loading indicator without requiring JavaScript.
