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 between transform: rotate() and rotate:. 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.

rotate:
.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-origin and watch where the pivot point moves. (The pin is just a visual helper.)

Pivot party

Try 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 element

A friendly card

Rotation changes the paint, not the layout.

Neighbor element

This 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 how transform-origin and container overflow change the result.
Random example photo
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-origin to “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-block is 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 has perspective, which makes depth visible.

3D Card

Try 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, try display: 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 transform in two different rules, the last one wins. Consider combining them in one declaration or use rotate: with other individual transform properties.
  • 3D looks flat: add perspective (often on the parent) and consider backface-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.