
This CSS-based 3D text spinner allows you to create an interactive rotating text display with mouse-tracking capabilities.
It rotates characters in a 3D space while maintaining smooth transitions between user interactions. You can modify the text content by updating the data-character attributes in the HTML structure.
You can also customize the spinner’s appearance to your specific needs by adjusting the font, colors, animation speed, and dimensions through a set of CSS variables introduced below.
How to use it:
1. Set up the HTML structure:
- Create the individual characters for your text spinner. Each character is added within a
lielement, and eachlielement is assigned adata-characterattribute. To add a new character, simply replicate the structure of the existinglielements and modify thedata-characteraccordingly. - Use the
--_num-charactersvariable to specify the total number of characters (including spaces). This variable ensures the spinner is properly sized and aligned to display all characters in the correct order. - You will also need to add the controls for the spinner, which allow users to interact with the animation using a mouse. These controls are implemented as radio buttons. The number of controls should correspond to the number of characters in the spinner (excluding spaces).
<div class="spinner inner-reverse" style="--_num-controls: 21">
<div class="spinner-control-button" style="--index: 1" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 2" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 3" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 4" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 5" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 6" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 7" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 8" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 9" ><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 10"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 11"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 12"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 13"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 14"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 15"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 16"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 17"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 18"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 19"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 20"><input type="radio" name="spinner-control-input"></div>
<div class="spinner-control-button" style="--index: 21"><input type="radio" name="spinner-control-input"></div>
<!-- Replace characters here -->
<div class="spinner-user-rotation">
<div class="spinner-text-wrapper">
<ul class="spinner-text" style="--_num-characters: 26">
<li class="spinner-character" style="--_index: 1" data-character="C"></li>
<li class="spinner-character" style="--_index: 2" data-character="S"></li>
<li class="spinner-character" style="--_index: 3" data-character="S"></li>
<li class="spinner-character" style="--_index: 4" data-character=" "></li>
<li class="spinner-character" style="--_index: 5" data-character="S"></li>
<li class="spinner-character" style="--_index: 6" data-character="C"></li>
<li class="spinner-character" style="--_index: 7" data-character="R"></li>
<li class="spinner-character" style="--_index: 8" data-character="I"></li>
<li class="spinner-character" style="--_index: 9" data-character="P"></li>
<li class="spinner-character" style="--_index: 10" data-character="T"></li>
<li class="spinner-character" style="--_index: 11" data-character=" "></li>
<li class="spinner-character" style="--_index: 12" data-character="."></li>
<li class="spinner-character" style="--_index: 13" data-character="C"></li>
<li class="spinner-character" style="--_index: 14" data-character="O"></li>
<li class="spinner-character" style="--_index: 15" data-character="M"></li>
<li class="spinner-character" style="--_index: 16" data-character=" "></li>
<li class="spinner-character" style="--_index: 17" data-character="L"></li>
<li class="spinner-character" style="--_index: 18" data-character="I"></li>
<li class="spinner-character" style="--_index: 19" data-character="V"></li>
<li class="spinner-character" style="--_index: 20" data-character="E"></li>
<li class="spinner-character" style="--_index: 21" data-character=" "></li>
<li class="spinner-character" style="--_index: 22" data-character="D"></li>
<li class="spinner-character" style="--_index: 23" data-character="E"></li>
<li class="spinner-character" style="--_index: 24" data-character="M"></li>
<li class="spinner-character" style="--_index: 25" data-character="O"></li>
<li class="spinner-character" style="--_index: 26" data-character=" "></li>
</ul>
</div>
</div>
</div>2. Use the following CSS rules to activate the 3D spinner effect. The key is to customize the --spinner-diameter, --spinner-font-size, and other variables to match your desired appearance.
/*
vars
*/
:root {
--spinner-diameter: 50rem;
--spinner-3d-perspective: 1000px;
--spinner-font-family: 'Roboto Mono', monospace;
--spinner-font-weight: 700;
--spinner-font-size: 11rem;
--spinner-font-color-outer-rbg: 233, 225, 224;
--spinner-font-color-inner-rbg-from: rgb(217, 94, 39);
--spinner-font-color-inner-rbg-to: rgb(255, 182, 68);
--spinner-animation-duration-outer: 10s;
--spinner-animation-duration-inner: 6s;
--spinner-controls-diameter: 256rem;
--spinner-control-bg-color: transparent;
--spinner-control-bg-hover-color: transparent;
--spinner-control-start-rotation: 0;
--spinner-transition-user-duration: 1000ms;
--spinner-transition-user-ease: ease;
}
@property --spinner-font-color-inner {
syntax: "";
inherits: true;
initial-value: rgb(217, 94, 39);
}
/*
spinner
*/
.spinner {
--_control-diamater: var(--spinner-controls-diameter);
--_control-radius: calc(var(--_control-diamater) * 0.5);
--_current-rotation: var(--spinner-control-start-rotation);
--_font-size: var(--spinner-font-size);
--_character-diameter: calc(var(--spinner-font-size) * 0.66);
--_diameter: var(--spinner-diameter);
--_radius: calc(var(--_diameter) * 0.5);
--_z: calc(var(--_radius) * -1);
--_rotate-x-to: 360deg;
width: var(--_diameter);
height: var(--_diameter);
perspective: var(--spinner-3d-perspective);
font-family: var(--spinner-font-family);
font-weight: var(--spinner-font-weight);
font-size: var(--_font-size);
position: relative;
/* overflow: hidden; */
opacity: 0;
animation: spinner-intro forwards 1s ease-out;
}
@keyframes spinner-intro {
from {opacity: 0;}
to {opacity: 1;}
}
@media only screen and (max-width: 50rem) {
.spinner {
--_font-size: calc(var(--spinner-font-size) * 0.75);
--_diameter: calc(var(--spinner-diameter) * 0.75);
}
}
@media only screen and (max-width: 38rem) {
.spinner {
--_font-size: calc(var(--spinner-font-size) * 0.6);
--_diameter: calc(var(--spinner-diameter) * 0.6);
}
}
@media only screen and (max-width: 30rem) {
.spinner {
--_font-size: calc(var(--spinner-font-size) * 0.25);
--_diameter: calc(var(--spinner-diameter) * 0.25);
}
}
@media only screen and (max-width: 15rem) {
.spinner {
--_font-size: calc(var(--spinner-font-size) * 0.1);
--_diameter: calc(var(--spinner-diameter) * 0.1);
}
}
.spinner .spinner-control-button {
--_width: var(--_control-radius);
--_height: calc(var(--_control-diamater) * 3.141592653589793 / var(--_num-controls) + 10px);
--_theta-start: 0;
--_theta-length: calc(2 * 3.141592653589793);
--_segment: calc(var(--_theta-start) + var(--index) / var(--_num-controls) * var(--_theta-length));
--_x: calc(var(--_radius) - var(--_control-radius) + var(--_control-radius) * cos(var(--_segment)));
--_y: calc(var(--_radius) - var(--_control-radius) + var(--_control-radius) * sin(var(--_segment)) + var(--_control-radius) - var(--_height) / 2);
--_rotation: calc(var(--index) / var(--_num-controls) * 360deg);
position: absolute;
left: var(--_x);
top: var(--_y);
width: var(--_width);
height: var(--_height);
clip-path: polygon(0% 50%, 100% 0%, 100% 100%);
transform-origin: right center;
transform: rotate(var(--_rotation));
background-color: var(--spinner-control-bg-color);
}
.spinner .spinner-control-button input {
-webkit-appearance: none;
appearance: none;
opacity: 0;
width: 100%;
height: 100%;
}
.spinner .spinner-control-button:has(input:hover) {
background-color: var(--spinner-control-bg-hover-color);
}
.spinner:has(.spinner-control-button:nth-child(1) input:hover) {
--_current-rotation: 108;
}
.spinner:has(.spinner-control-button:nth-child(2) input:hover) {
--_current-rotation: 126;
}
.spinner:has(.spinner-control-button:nth-child(3) input:hover) {
--_current-rotation: 144;
}
.spinner:has(.spinner-control-button:nth-child(4) input:hover) {
--_current-rotation: 162;
}
.spinner:has(.spinner-control-button:nth-child(5) input:hover) {
--_current-rotation: 180;
}
.spinner:has(.spinner-control-button:nth-child(6) input:hover) {
--_current-rotation: 198;
}
.spinner:has(.spinner-control-button:nth-child(7) input:hover) {
--_current-rotation: 216;
}
.spinner:has(.spinner-control-button:nth-child(8) input:hover) {
--_current-rotation: 234;
}
.spinner:has(.spinner-control-button:nth-child(9) input:hover) {
--_current-rotation: 252;
}
.spinner:has(.spinner-control-button:nth-child(10) input:hover) {
--_current-rotation: 270;
}
.spinner:has(.spinner-control-button:nth-child(11) input:hover) {
--_current-rotation: 288;
}
.spinner:has(.spinner-control-button:nth-child(12) input:hover) {
--_current-rotation: 306;
}
.spinner:has(.spinner-control-button:nth-child(13) input:hover) {
--_current-rotation: 324;
}
.spinner:has(.spinner-control-button:nth-child(14) input:hover) {
--_current-rotation: 342;
}
.spinner:has(.spinner-control-button:nth-child(15) input:hover) {
--_current-rotation: 0;
}
.spinner:has(.spinner-control-button:nth-child(16) input:hover) {
--_current-rotation: 18;
}
.spinner:has(.spinner-control-button:nth-child(17) input:hover) {
--_current-rotation: 36;
}
.spinner:has(.spinner-control-button:nth-child(18) input:hover) {
--_current-rotation: 54;
}
.spinner:has(.spinner-control-button:nth-child(19) input:hover) {
--_current-rotation: 72;
}
.spinner:has(.spinner-control-button:nth-child(20) input:hover) {
--_current-rotation: 90;
}
.spinner .spinner-control-button:has(input:hover) ~ .spinner-user-rotation {
transform: rotate(calc(calc(var(--_current-rotation) - 90) * 1deg));
--_rotate-x-to: 0deg;
}
.spinner .spinner-user-rotation {
width: inherit;
height: inherit;
transform-style: preserve-3d;
pointer-events: none;
transform: rotate(calc(var(--_current-rotation) * 1deg));
transition: transform var(--spinner-transition-user-duration) var(--spinner-transition-user-ease);
}
.spinner .spinner-text-wrapper {
width: inherit;
height: inherit;
animation: spinner-rotation-outer var(--spinner-animation-duration-outer) reverse linear infinite running,
spinner-rotation-color calc(var(--spinner-animation-duration-outer) * 0.5) reverse linear infinite running;
transform: translateZ(var(--_z));
transform-style: inherit;
}
.spinner .spinner-text {
width: inherit;
height: inherit;
list-style-type: none;
position: relative;
transform-style: inherit;
animation: spinner-rotation-inner var(--spinner-animation-duration-inner) reverse linear infinite running;
}
@keyframes spinner-rotation-color {
/* doesn't work on firefox: */
/* 0%, 100% {--spinner-font-color-inner: var(--spinner-font-color-inner-rbg-from);}
50% {--spinner-font-color-inner: var(--spinner-font-color-inner-rbg-to);} */
/* ugly workaround */
0%, 100% {--spinner-font-color-inner: rgb(217, 94, 39);}
50% {--spinner-font-color-inner: rgb(255, 182, 68);}
}
@keyframes spinner-rotation-outer {
from {transform: translateZ(var(--_z)) rotateX(0deg);}
to {transform: translateZ(var(--_z)) rotateX(var(--_rotate-x-to));}
/* try other variants: */
/* from {transform: translateZ(var(--_z)) rotateX(0deg);}
to {transform: translateZ(var(--_z)) rotateX(360deg);} */
/* from {transform: translateZ(var(--_z)) rotateY(0deg) rotateX(0deg);}
to {transform: translateZ(var(--_z)) rotateY(360deg) rotateX(var(--_rotate-x-to));} */
/* from {transform: translateZ(var(--_z)) rotateY(0deg) rotateX(0deg);}
to {transform: translateZ(var(--_z)) rotateY(360deg) rotateX(360deg);} */
}
@keyframes spinner-rotation-inner {
from {transform: rotateY(0deg);}
to {transform: rotateY(360deg);}
}
.spinner .spinner-character {
--_width: var(--_character-diameter);
--_height: var(--_character-diameter);
--_rotation: calc(360 / var(--_num-characters) * var(--_index) * 1deg);
position: absolute;
left: calc(var(--_radius) - var(--_character-diameter) / 2);
top: calc(var(--_radius) - var(--_character-diameter) / 2);
transform: rotateY(var(--_rotation)) translateZ(var(--_radius));
transform-style: inherit;
width: var(--_width);
height: var(--_height);
}
.spinner .spinner-character::before,
.spinner .spinner-character::after {
content: attr(data-character);
width: inherit;
height: inherit;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
.spinner.inner-reverse .spinner-character::before,
.spinner.inner-reverse .spinner-character::after {
backface-visibility: hidden;
}
.spinner .spinner-character::before {
/* transform: rotateY(180deg) rotateZ(180deg) rotateX(180deg) translateZ(-1px); */
transform: rotateY(180deg) rotateZ(180deg) rotateX(180deg);
/* transform: rotateY(0deg) rotateZ(180deg) translateZ(calc(var(--_diameter) * -1)); */
/* transform: rotateY(180deg) rotateZ(180deg); */
color: var(--spinner-font-color-inner);
}
.spinner .spinner-character::after {
backface-visibility: hidden;
transform: rotateY(0deg);
color: rgb(var(--spinner-font-color-outer-rbg));
}
.spinner.inner-reverse .spinner-character::before {
transform: rotateY(180deg) rotateZ(180deg);
}How it works:
This CSS-only spinner leverages several key CSS techniques:
CSS Variables: These variables allow for dynamic customization of the spinner’s appearance and behavior. By changing these variables, you can easily adjust the size, colors, fonts, and animation speeds without directly modifying the core CSS rules.
3D Transforms: The transform-style: preserve-3d and perspective properties create the 3D effect, giving the text depth and allowing for rotations around the Y-axis. The translateZ function positions the characters along the Z-axis to achieve the circular arrangement.
Animations and Transitions: CSS animations create the continuous rotation of the spinner. The spinner-rotation-outer and spinner-rotation-inner animations control the outer and inner rotations, respectively. Transitions provide smooth visual feedback when interacting with the mouse-aware controls.
Radio Buttons and :has() Selector: Radio buttons and the :has() selector are used together to implement the mouse-aware functionality. Hovering over a radio button changes the --_current-rotation variable, which then triggers the rotation of the text. This allows for interactive control of the spinner’s position without using JavaScript.







