What CSS rotate actually does
CSS rotation is a visual transform: it changes how an element is drawn, without changing the space it originally takes up in the layout.
That means a rotated element can overlap neighbors (because the layout box stays where it was), and it can also get clipped if its container has overflow: hidden.
Rotation is done around an axis (2D rotates around the Z axis, like a clock hand on the screen). Later we’ll also rotate in 3D with rotate3d().
Two ways to rotate: transform rotate vs rotate property
There are two common ways to rotate in modern CSS:
-
Classic:
transform: rotate(20deg); -
Newer “individual transform”:
rotate: 20deg;
The rotate property is part of the “individual transform properties” family (translate, rotate, scale).
Browser support is generally good in modern browsers, but you should still check for your audience:
Can I use: rotate.
Practical rule of thumb: if you need maximum compatibility, transform: rotate() is the safe classic.
If you like clearer, composable transforms, rotate: is very nice.
.demo {
transform: rotate(-10deg);
}
.demo {
rotate: -10deg;
}
.demo {
transform: rotate(-10deg);
rotate: -10deg;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
max-width: 920px;
}
.note {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
background: #fff;
}
.stage {
display: grid;
place-items: center;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f6f6f6;
min-height: 220px;
}
.demo {
width: 240px;
height: 120px;
border: 3px solid #111;
border-radius: 16px;
background: #fff;
display: grid;
place-items: center;
text-align: center;
font-weight: 700;
box-shadow: 0 10px 0 rgba(0, 0, 0, 0.12);
}
.demo small {
display: block;
font-weight: 600;
opacity: 0.75;
}
Click the snippets to switch betweentransform: rotate()androtate:. In the third snippet, both are set to show how they can stack visually.Rotating box Look mom, no elbows
Angles and quick rotations: 90° and 180°
Rotation uses angle units. The most common is deg (degrees), where:
- 90deg = quarter turn
- 180deg = half turn
- 360deg = full turn
You may also see turn (where 1turn = 360deg) and rad (radians).
For most people, degrees are the most readable.
.arrow {
rotate: 0deg;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
}
.panel {
display: grid;
gap: 12px;
padding: 16px;
border: 3px solid #111;
border-radius: 16px;
background: #f6f6f6;
}
.row {
display: grid;
gap: 10px;
align-items: center;
}
.arrow {
width: 220px;
height: 90px;
border: 3px solid #111;
border-radius: 16px;
background: #fff;
display: grid;
place-items: center;
font-weight: 800;
letter-spacing: 0.03em;
}
.arrow::before {
content: "➜";
font-size: 44px;
line-height: 1;
}
This uses
rotate:so you can see the common “rotate 90 degrees” and “rotate 180 degrees” values instantly.
transform-origin: where the rotation pivots
By default, rotation happens around the element’s center. But sometimes you want it to rotate around a corner (like a door hinge).
That’s what transform-origin is for.
transform-origin accepts keywords (center, top left, etc.) or lengths/percentages (0% 0%, 50% 100%).
It sets the “pivot point” inside the element. By default, it’s 50% 50% (the center).
.stage {
--pin-x: 50%;
--pin-y: 50%;
}
.card {
transform-origin: 50% 50%;
transform: rotate(-18deg);
}
.stage {
--pin-x: 0%;
--pin-y: 0%;
}
.card {
transform-origin: 0% 0%;
transform: rotate(-18deg);
}
.stage {
--pin-x: 100%;
--pin-y: 0%;
}
.card {
transform-origin: 100% 0%;
transform: rotate(-18deg);
}
.stage {
--pin-x: 0%;
--pin-y: 100%;
}
.card {
transform-origin: 0% 100%;
transform: rotate(-18deg);
}
.stage {
--pin-x: 100%;
--pin-y: 100%;
}
.card {
transform-origin: 100% 100%;
transform: rotate(-18deg);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
}
.stage {
display: grid;
place-items: center;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f6f6f6;
min-height: 280px;
position: relative;
}
.card {
width: 260px;
height: 160px;
border: 3px solid #111;
border-radius: 18px;
background: #fff;
display: grid;
place-items: center;
text-align: center;
padding: 14px;
box-shadow: 0 14px 0 rgba(0, 0, 0, 0.12);
position: relative;
}
.pin {
width: 14px;
height: 14px;
border: 3px solid #111;
border-radius: 999px;
background: #fff;
position: absolute;
left: calc(var(--pin-x) - 7px);
top: calc(var(--pin-y) - 7px);
}
.hint {
font-size: 14px;
opacity: 0.75;
margin: 0;
}
Click the snippets to switch
transform-originand watch where the pivot point moves. (The pin is just a visual helper.)Pivot partyTry each corner for a hinge effect.
Rotate an element
Rotating a normal element is straightforward. The biggest “gotcha” is remembering that rotation doesn’t reflow the layout. So if your rotated element overlaps something, that’s normal. You can add spacing or use a wrapper to manage the layout.
.tile {
rotate: -20deg;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
display: grid;
gap: 12px;
}
.grid {
display: grid;
gap: 12px;
grid-template-columns: 1fr;
}
@media (min-width: 820px) {
.grid {
grid-template-columns: 1fr 1fr;
}
}
.tile {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 18px;
display: grid;
gap: 10px;
box-shadow: 0 14px 0 rgba(0, 0, 0, 0.12);
}
.tile h4 {
margin: 0;
font-size: 18px;
}
.tile p {
margin: 0;
opacity: 0.8;
}
.neighbor {
border: 3px dashed #111;
border-radius: 18px;
background: #f6f6f6;
padding: 18px;
}
.badge {
display: inline-block;
border: 2px solid #111;
border-radius: 999px;
padding: 6px 10px;
font-weight: 700;
background: #fff;
}
Drag the slider. Notice how the rotated tile can visually push into the dashed neighbor even though the layout boxes haven’t moved.
Rotated elementA friendly card
Rotation changes the paint, not the layout.
Neighbor elementThis box stays put in layout land.
CSS rotate image
Images rotate like any other element. If you rotate inside a frame, you might see corners get clipped.
That’s usually because the frame uses overflow: hidden (often for nice rounded corners).
.photo img {
transform: rotate(-12deg);
}
.photo img {
transform: rotate(-12deg);
transform-origin: left bottom;
}
.photo {
overflow: visible;
}
.photo img {
transform: rotate(-12deg);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
display: grid;
gap: 12px;
}
.note {
border: 2px dashed #111;
border-radius: 14px;
padding: 12px;
background: #fff;
}
.row {
display: grid;
gap: 12px;
align-items: start;
}
@media (min-width: 860px) {
.row {
grid-template-columns: 1.1fr 0.9fr;
}
}
.photo {
border: 3px solid #111;
border-radius: 18px;
overflow: hidden;
background: #f6f6f6;
padding: 16px;
}
.photo img {
width: 100%;
height: 260px;
object-fit: cover;
display: block;
border-radius: 12px;
border: 3px solid #111;
background: #fff;
}
.tips {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 16px;
}
.tips ul {
margin: 8px 0 0;
padding-left: 18px;
}
This demo uses a random Picsum image. Switch snippets to see howtransform-originand containeroverflowchange the result.
Common fixes
- Rotate the image or rotate the frame, depending on the effect you want.
- If corners get clipped, check the parent’s
overflow.- Use
transform-originto “pin” rotation where it makes sense.
CSS rotate text
Text can be rotated too, but there’s a beginner trap: inline elements (like a plain span)
don’t always behave the way you expect with transforms.
The simple fix is to give the element a layout box: display: inline-block;.
Then rotation is predictable.
.tag {
transform: rotate(-8deg);
}
.tag {
display: inline-block;
transform: rotate(-8deg);
}
.tag {
display: inline-block;
rotate: -8deg;
transform-origin: left center;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
display: grid;
gap: 12px;
}
.card {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
padding: 18px;
display: grid;
gap: 10px;
}
.tag {
border: 2px solid #111;
border-radius: 999px;
padding: 6px 12px;
font-weight: 800;
background: #f6f6f6;
}
p {
margin: 0;
opacity: 0.85;
}
Here is a rotated label inside normal text. Switch snippets to see why
inline-blockis often your best friend.Rotated text is great for stickers, ribbons, “NEW!” badges, and playful UI flourishes.
Learn more about display: inline; in the CSS Display Inline Interactive Tutorial.
CSS rotate background image
CSS cannot rotate a background image by itself as a separate layer (there’s no background-rotate property).
The normal approach is: rotate the element that has the background.
If you need a background to rotate independently, you can place an actual element (like a ::before pseudo-element)
behind the content and rotate that.
.panel {
transform: rotate(-6deg);
}
.panel::before {
transform: rotate(12deg);
}
.panel::before {
transform: rotate(12deg) scale(2.4);
transform-origin: 70% 70% ;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
}
.panel {
position: relative;
border: 3px solid #111;
border-radius: 18px;
overflow: hidden;
padding: 18px;
background: #fff;
min-height: 240px;
display: grid;
place-items: center;
}
.panel::before {
content: "";
position: absolute;
inset: -40px;
background-image: url("https://picsum.photos/1000/800");
background-size: cover;
background-position: center;
opacity: 0.75;
transform: rotate(0deg);
}
.panel::after {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.55);
}
.content {
position: relative;
z-index: 1;
border: 3px solid #111;
border-radius: 16px;
padding: 14px 16px;
background: #fff;
max-width: 520px;
text-align: center;
}
.content h4 {
margin: 0;
font-size: 18px;
}
.content p {
margin: 8px 0 0;
opacity: 0.85;
}
Snippet 1 rotates the whole panel (including content). Snippets 2 and 3 rotate only the background layer (
::before).Rotating backgrounds
Rotate the element, or rotate a separate background layer behind the content.
Learn more about pseudo-elements in the CSS ::before and ::after Pseudo-Elements Interactive Tutorial.
CSS rotate animation
Animating rotation is perfect for spinners, playful icons, loading states, and subtle attention cues.
You can animate transform or animate the rotate property. Both work; pick the one you’re using elsewhere.
For accessibility, consider respecting reduced motion preferences with prefers-reduced-motion.
.spinner {
animation: spin 1.2s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.spinner {
animation: wobble 900ms ease-in-out infinite;
}
@keyframes wobble {
0% {
transform: rotate(-14deg);
}
50% {
transform: rotate(14deg);
}
100% {
transform: rotate(-14deg);
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
display: grid;
gap: 12px;
}
.stage {
display: grid;
place-items: center;
border: 3px solid #111;
border-radius: 16px;
background: #f6f6f6;
min-height: 260px;
}
.spinner {
width: 140px;
height: 140px;
border-radius: 999px;
border: 3px solid #111;
background: #fff;
display: grid;
place-items: center;
position: relative;
}
.spinner::before {
content: "";
width: 16px;
height: 16px;
border-radius: 999px;
border: 3px solid #111;
background: #fff;
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
}
.spinner strong {
font-size: 16px;
}
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none !important;
}
}
Try the snippets: a classic spinner (transform), a spinner using
rotate:, and a friendly wobble.Loading
Learn more about CSS animations in the CSS Animation Interactive Tutorial.
rotate3d and perspective (welcome to the third dimension)
3D rotation is where things get really fun. In 3D, rotation needs perspective to look “real”. Without perspective, the browser can still rotate the element in 3D space, but it looks flat and less dramatic.
The rotate3d syntax is rotate3d(x, y, z, angle), where the first three parameters define the axis of rotation in 3D space.
Perspective can be applied in two common ways:
- On the parent:
perspective: 900px;(very common) - Inside transform:
transform: perspective(900px) rotate3d(...)
We’ll use the parent method, because it’s easier to reason about: “this container is the camera”.
.scene {
perspective: 900px;
}
.card {
transform: rotate3d(0, 1, 0, 50deg);
}
.scene {
perspective: 900px;
}
.card {
transform: rotate3d(1, 0, 0, 60deg);
}
.scene {
perspective: 900px;
}
.card {
transform: rotate3d(1, 1, 0, 60deg);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
padding: 18px;
font-family: system-ui, Arial, sans-serif;
max-width: 920px;
display: grid;
gap: 12px;
}
.scene {
border: 3px solid #111;
border-radius: 16px;
background: #f6f6f6;
min-height: 320px;
display: grid;
place-items: center;
padding: 18px;
}
.card {
width: 260px;
height: 170px;
border: 3px solid #111;
border-radius: 18px;
background: #fff;
display: grid;
place-items: center;
text-align: center;
padding: 14px;
box-shadow: 0 16px 0 rgba(0, 0, 0, 0.12);
backface-visibility: hidden;
}
.card p {
margin: 8px 0 0;
opacity: 0.85;
}
Each snippet rotates in 3D using
rotate3d(x, y, z, angle). The parent hasperspective, which makes depth visible.3D CardTry rotating around X, Y, or both.
Try editing in different perspective values to see how it affects the 3D rotation.
Common “rotate not working” fixes
-
You rotated an inline element:
if it’s a
span, trydisplay: inline-block;. -
It looks clipped:
check parent containers for
overflow: hidden. - You expected layout to move: transforms don’t affect layout flow. Add spacing or use a wrapper.
-
Transforms got overwritten:
if you set
transformin two different rules, the last one wins. Consider combining them in one declaration or userotate:with other individual transform properties. -
3D looks flat:
add
perspective(often on the parent) and considerbackface-visibility: hidden;.
Wrap-up
You now have a full rotation toolkit:
transform: rotate() for classic compatibility, rotate: for modern clarity, transform-origin for pivot control,
animation for motion, and rotate3d() plus perspective for depth.
Learn more about CSS transforms in general in the CSS Transform Interactive Tutorial.
