要素を回転させる
方法は 2 つで、結果は同じです。transform の中の rotate() 関数か、単独の rotate プロパティです。正の角度は時計回りです:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rotate</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.row { display: flex; gap: 50px; padding: 30px; }
.box { width: 90px; height: 60px; display: grid; place-items: center; color: white; }
.a { transform: rotate(45deg); background: #2563eb; }
.b { rotate: 45deg; background: #16a34a; }
.c { rotate: -15deg; background: #f59e0b; color: #111827; }
</style>
</head>
<body>
<div class="row">
<div class="box a">transform</div>
<div class="box b">rotate: 45deg</div>
<div class="box c">-15deg</div>
</div>
</body>
</html>
青と緑のボックスは、どちらも中心を軸に時計回りに 45 度、まったく同じように回転しています。琥珀色のボックスは負の角度なので、反時計回りに傾きます。回転はレイアウトを動かしません。どのボックスも元の 90 × 60 の枠を取ったままで、回転した要素が隣と重なることがあるのはこのためです。
角度の単位
| 単位 | 1 周 | 90 度 |
|---|
deg | 360deg | 90deg |
turn | 1turn | 0.25turn |
rad | 6.2832rad | 1.5708rad |
grad | 400grad | 100grad |
普通は deg を使います。回転し続けるものには turn が読みやすいです (rotate: 1turn)。360 を超える角度も使え、アニメーションでは意味があります。キーフレームの rotate(720deg) は 2 回転します。単位は必ず書きましょう。rotate(45) は無効で、無視されます。
スライダーをドラッグして、各単位での角度を見てください:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angle slider</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 14px; }
.stage { display: grid; place-items: center; height: 160px; background: #f3f4f6; margin: 10px 0; }
#arrow { width: 110px; height: 14px; background: #2563eb; border-radius: 7px; position: relative; }
#arrow::after { content: ""; position: absolute; inset-inline-end: -10px; top: -8px; border: 15px solid transparent; border-inline-start: 18px solid #2563eb; border-inline-end: 0; }
</style>
</head>
<body>
<label>angle <input id="a" type="range" min="-360" max="360" value="30"></label>
<div class="stage"><div id="arrow"></div></div>
<p id="out"></p>
<script>
const input = document.getElementById('a');
const update = () => {
const deg = Number(input.value);
document.getElementById('arrow').style.rotate = deg + 'deg';
document.getElementById('out').textContent =
deg + 'deg = ' + (deg / 360).toFixed(3) + 'turn = ' + (deg * Math.PI / 180).toFixed(4) + 'rad';
};
input.addEventListener('input', update);
update();
</script>
</body>
</html>
要素は初期状態では中心を軸に回転します。transform-origin で軸を動かせます。時計の針は、真ん中ではなく端を軸にします:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>transform-origin</title>
<style>
.face { position: relative; width: 160px; height: 160px; border-radius: 50%; background: #e5e7eb; }
.hand {
position: absolute;
left: 76px; /* 80px center minus half the 8px width */
top: 20px;
width: 8px;
height: 60px;
border-radius: 4px;
background: #111827;
transform-origin: bottom center; /* pivot at the center of the face */
rotate: 120deg;
}
.pin { position: absolute; left: 74px; top: 74px; width: 12px; height: 12px; border-radius: 50%; background: #f59e0b; }
</style>
</head>
<body>
<div class="face"><div class="hand" id="hand"></div><div class="pin"></div></div>
</body>
</html>
針の下の端が文字盤の中心の琥珀色のピンの上にあり、rotate: 120deg で 4 時の方向を向きます。transform-origin の行を消すと、針はピンから外れて、自分の真ん中を軸に回転します。
文字の回転と縦向きのラベル
バッジやステッカーを少し傾けるなら rotate で十分です。本当に縦向きのラベルには writing-mode のほうが適しています。レイアウトが変わり、要素が隣と重なる代わりに縦長で細くなるからです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vertical text</title>
<style>
body { font-family: system-ui, sans-serif; padding-top: 24px; }
.row { display: flex; gap: 12px; align-items: flex-start; }
.label { background: #2563eb; color: white; padding: 6px 10px; }
.rotated { rotate: -90deg; }
.vertical { writing-mode: vertical-rl; rotate: 180deg; }
.content { background: #e5e7eb; padding: 10px; }
.badge { display: inline-block; rotate: -6deg; background: #f59e0b; padding: 4px 10px; margin: 16px 0 0 8px; }
</style>
</head>
<body>
<div class="row"><div class="label rotated" id="r">Section</div><div class="content">rotate: -90deg overlaps</div></div>
<div class="row" style="margin-top: 20px;"><div class="label vertical" id="v">Section</div><div class="content">writing-mode takes vertical space</div></div>
<span class="badge">New</span>
<p id="out"></p>
<script>
const size = (id) => { const el = document.getElementById(id); return el.offsetWidth + ' x ' + el.offsetHeight; };
document.getElementById('out').textContent = 'layout box: rotated ' + size('r') + ', writing-mode ' + size('v');
</script>
</body>
</html>
表示欄は、各ラベルのレイアウト上のサイズを示しています。回転させたほうは、元の横長で背の低いボックスのままです。横に空きを残し、行の上下にはみ出して、そこにあるものと重なります。writing-mode のほうは縦長で細いボックスを取ります。rotate: 180deg を加えると下から上に読む向きになり、縦向きのラベルではこれが一般的です。
クリックやホバーでアイコンを回転させる
矢印を 180 度回転させるのは、セクションが開いていることを示す定番の方法です。rotate と transition を組み合わせます:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Rotating arrow</title>
<style>
body { font-family: system-ui, sans-serif; }
.toggle { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; font: inherit; border: 1px solid #111827; background: white; border-radius: 6px; }
.chevron { width: 10px; height: 10px; border-inline-end: 2px solid #111827; border-bottom: 2px solid #111827; rotate: 45deg; transition: rotate 0.25s ease-out; }
.toggle[aria-expanded="true"] .chevron { rotate: 225deg; }
</style>
</head>
<body>
<button class="toggle" aria-expanded="false">Details <span class="chevron" aria-hidden="true"></span></button>
<script>
const btn = document.querySelector('.toggle');
btn.addEventListener('click', () => btn.setAttribute('aria-expanded', btn.getAttribute('aria-expanded') !== 'true'));
</script>
</body>
</html>
山形のアイコンは border が 2 辺の正方形で、45 度回転させて下を向けています。ボタンが展開されると 225 度になって上を向き、transition が半回転をアニメーションさせます。スタイルは aria-expanded 属性で切り替えるので、見た目の状態とスクリーンリーダーが読み上げる状態が食い違うことはありません。
回り続けるローディング
回転し続ける動きは 0 度から 360 度への回転で、速さが変わらないように linear で繰り返します:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Spinner</title>
<style>
@keyframes spin {
to { rotate: 360deg; }
}
.spinner {
width: 36px;
height: 36px;
border: 4px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.spinner { animation-duration: 3s; } /* slower, still shows activity */
}
</style>
</head>
<body>
<div class="spinner" role="status" aria-label="Loading"></div>
</body>
</html>
to のキーフレームだけなので、アニメーションは要素自身の回転 (なし) から始まって 360deg で終わり、360deg は 0 と同じに見えるので、infinite で飛ばずに繰り返されます。システムで動きを減らす設定にしている人には、スピナーが 3 倍遅く回ります。キーフレームについては animation のページ で詳しく解説しています。
3D 回転: rotateX、rotateY
rotateX() は横の軸を中心に要素を奥に倒し、rotateY() はドアのように縦の軸を中心に回します。遠近感がないとつぶれたように見えるだけなので、親の perspective で奥行きを加えます:
.scene { perspective: 600px; }
.card { transform: rotateY(35deg); }
.flip { transform: rotateY(180deg); backface-visibility: hidden; } /* card flip */
要素を回転ではなく反転させたいなら、scale: -1 1 (左右反転) を使います。180 度の回転は上下逆さまになるので、同じではありません。
よくある間違い
<span> や <a> を回転させても動かない。 普通のインライン要素は transform を無視します。display: inline-block を追加しましょう。
- ホバーで回転が消える。
transform: scale(1.1) のホバーのルールが transform: rotate(...) を置き換えています。個別の rotate と scale のプロパティを使うか、両方の関数を並べましょう。
- レイアウトが場所を空けてくれると思う。 回転させた画像は回転前のボックスを保ち、隣と重なります。コンテナに回転後のサイズを与えるか、文字なら
writing-mode を使いましょう。
- スピナーに
ease を使う。 初期値のタイミングでは、1 回転ごとに加速と減速をします。linear を使いましょう。
- 違う点を軸に回転させる。 角度を調整する前に
transform-origin を指定しましょう。