CSS Trigonometric Functions(三角関数)で円形アニメーションを作ってみた
CSSで三角関数が使えるようになったの、ご存知ですか? sin()、cos()、tan() といった数学関数がCSSネイティブで使えるんですよね。今回はこれを使って円形の配置やアニメーションをいろいろ作ってみました。
▶ サンプルページを見る(円形配置・円軌道・波形・放射状メニューのインタラクティブデモ)
目次
CSSで三角関数が使えるようになった
CSS Values and Units Module Level 4で、三角関数が正式にサポートされました。使える関数は以下の通りです。
sin()… サイン(正弦)cos()… コサイン(余弦)tan()… タンジェント(正接)asin()… アークサインacos()… アークコサインatan()… アークタンジェントatan2()… 2引数アークタンジェント
今まではJavaScriptで計算してstyleを当てるしかなかったんですが、CSSだけで完結できるようになったのはかなり嬉しいですね。
基本的な使い方と単位
三角関数の引数には角度を渡します。CSSで使える角度の単位は3種類あります。
deg… 度数法(360deg = 1周)rad… ラジアン(2πrad = 1周)turn… 回転数(1turn = 1周)
個人的には turn が直感的で使いやすいと感じました。1turnで1周なので、0.25turnで90度、0.5turnで180度みたいな感覚で書けます。
基本的な書き方はこんな感じです。
.element {
/* sin()の戻り値は -1 〜 1 の数値 */
/* calc()と組み合わせてpxやemに変換する */
transform: translateX(calc(sin(45deg) * 100px));
transform: translateY(calc(cos(0.25turn) * 100px));
}sin() や cos() の戻り値は -1 〜 1 の単位なしの数値です。なので、calc() と組み合わせて実際の長さに変換してあげる必要がありますね。
実例1: 要素を円形に配置する(時計の文字盤風)
まずは定番の、要素を円形に並べるレイアウトを作ってみました。時計の文字盤みたいに12個の数字を円上に配置します。
<div class="clock">
<span style="--i: 1">1</span>
<span style="--i: 2">2</span>
<span style="--i: 3">3</span>
<span style="--i: 4">4</span>
<span style="--i: 5">5</span>
<span style="--i: 6">6</span>
<span style="--i: 7">7</span>
<span style="--i: 8">8</span>
<span style="--i: 9">9</span>
<span style="--i: 10">10</span>
<span style="--i: 11">11</span>
<span style="--i: 12">12</span>
</div>.clock {
position: relative;
width: 300px;
height: 300px;
border: 2px solid #333;
border-radius: 50%;
margin: 50px auto;
}
.clock span {
--angle: calc(var(--i) * (1turn / 12));
--radius: 120px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%)
translateX(calc(sin(var(--angle)) * var(--radius)))
translateY(calc(cos(var(--angle)) * var(--radius) * -1));
font-size: 1.2rem;
font-weight: bold;
}ポイントは --angle の計算です。カスタムプロパティ --i に1〜12の数値を渡して、1turn / 12 で均等に分割しています。sin() でX座標、cos() でY座標を求めて、translate で配置する仕組みですね。
CSSの座標系はY軸が下向きなので、cos()に * -1 をかけて反転させています。これを忘れると上下が逆になるので注意です。
実例2: 円軌道のアニメーション
次は @keyframes と組み合わせて、要素が円軌道を描くアニメーションを作ってみました。
<div class="orbit-container">
<div class="center-point"></div>
<div class="orbiting-element"></div>
</div>.orbit-container {
position: relative;
width: 300px;
height: 300px;
margin: 50px auto;
}
.center-point {
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
background: #333;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.orbiting-element {
--radius: 120px;
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 24px;
background: #e74c3c;
border-radius: 50%;
animation: orbit 3s linear infinite;
}
@keyframes orbit {
from {
transform: translate(-50%, -50%)
translateX(calc(sin(0turn) * var(--radius)))
translateY(calc(cos(0turn) * var(--radius) * -1));
}
to {
transform: translate(-50%, -50%)
translateX(calc(sin(1turn) * var(--radius)))
translateY(calc(cos(1turn) * var(--radius) * -1));
}
}…と書きたいところなんですが、実はこれだけだとうまく動きません。@keyframes の中で sin() の引数を0turnから1turnに変化させても、CSSはその中間値を補間してくれないんですよね。
そこで、@property を使ってカスタムプロパティをアニメーション可能にします。
@property --angle {
syntax: "<angle>";
initial-value: 0turn;
inherits: false;
}
.orbiting-element {
--radius: 120px;
--angle: 0turn;
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 24px;
background: #e74c3c;
border-radius: 50%;
transform: translate(-50%, -50%)
translateX(calc(sin(var(--angle)) * var(--radius)))
translateY(calc(cos(var(--angle)) * var(--radius) * -1));
animation: orbit 3s linear infinite;
}
@keyframes orbit {
to {
--angle: 1turn;
}
}@property で --angle を <angle> 型として登録すると、CSSが中間値を補間できるようになります。これで0turnから1turnまで滑らかにアニメーションしてくれます。
この @property との組み合わせはかなり重要なテクニックなので覚えておくと便利ですね。
実例3: 波形アニメーション(sin()で上下に揺れる)
複数の要素をsin()で揺らすと、きれいな波形アニメーションが作れます。各要素に位相のずれを持たせるのがコツです。
<div class="wave">
<span style="--i: 0"></span>
<span style="--i: 1"></span>
<span style="--i: 2"></span>
<span style="--i: 3"></span>
<span style="--i: 4"></span>
<span style="--i: 5"></span>
<span style="--i: 6"></span>
<span style="--i: 7"></span>
<span style="--i: 8"></span>
<span style="--i: 9"></span>
</div>@property --wave-angle {
syntax: "<angle>";
initial-value: 0turn;
inherits: false;
}
.wave {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
height: 200px;
}
.wave span {
--phase: calc(var(--i) * 0.1turn);
--wave-angle: 0turn;
display: block;
width: 20px;
height: 20px;
background: #3498db;
border-radius: 50%;
transform: translateY(
calc(sin(calc(var(--wave-angle) + var(--phase))) * 50px)
);
animation: wave-move 2s ease-in-out infinite;
}
@keyframes wave-move {
to {
--wave-angle: 1turn;
}
}各要素に --i で番号を振り、--phase として位相のずれを計算しています。0.1turn ずつずらすことで、波が流れるように見えるんですよね。
sin() の値に 50px をかけて、上下の揺れ幅を制御しています。この値を変えれば波の振幅を調整できます。
実例4: 放射状メニュー(クリックで円形に展開)
ハンバーガーメニューをクリックすると、メニュー項目が円形に展開するUIを作ってみました。JavaScriptなしで、CSSだけで実装できます。
<div class="radial-menu">
<input type="checkbox" id="menu-toggle" class="menu-toggle" />
<label for="menu-toggle" class="menu-btn">+</label>
<a href="#" class="menu-item" style="--i: 0; --total: 5">🏠</a>
<a href="#" class="menu-item" style="--i: 1; --total: 5">🔍</a>
<a href="#" class="menu-item" style="--i: 2; --total: 5">⚙️</a>
<a href="#" class="menu-item" style="--i: 3; --total: 5">📧</a>
<a href="#" class="menu-item" style="--i: 4; --total: 5">👤</a>
</div>.radial-menu {
position: relative;
width: 300px;
height: 300px;
margin: 50px auto;
}
.menu-toggle {
display: none;
}
.menu-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 56px;
height: 56px;
background: #e74c3c;
color: #fff;
border-radius: 50%;
font-size: 2rem;
display: grid;
place-items: center;
cursor: pointer;
z-index: 10;
transition: transform 0.3s;
user-select: none;
}
.menu-toggle:checked + .menu-btn {
transform: translate(-50%, -50%) rotate(45deg);
}
.menu-item {
--angle: calc(var(--i) * (0.5turn / (var(--total) - 1)) - 0.25turn);
--radius: 0px;
position: absolute;
top: 50%;
left: 50%;
width: 44px;
height: 44px;
background: #3498db;
border-radius: 50%;
display: grid;
place-items: center;
text-decoration: none;
font-size: 1.2rem;
transform: translate(-50%, -50%)
translateX(calc(sin(var(--angle)) * var(--radius)))
translateY(calc(cos(var(--angle)) * var(--radius) * -1));
transition: --radius 0.4s ease-out, opacity 0.3s;
opacity: 0;
}
.menu-toggle:checked ~ .menu-item {
--radius: 100px;
opacity: 1;
}
@property --radius {
syntax: "<length>";
initial-value: 0px;
inherits: false;
}checkbox のハックを使って、クリック時の状態切り替えを実現しています。:checked になったら --radius を0pxから100pxに変更して、メニュー項目が中心から外側に広がるようにしました。
角度の計算では 0.5turn(180度)の範囲で上半分に扇状に展開するようにしています。 - 0.25turn で開始位置を調整しているところもポイントですね。
atan2()で角度計算
atan2() は2つの値からラジアン角度を返す関数です。XY座標から角度を求めたいときに使います。
/* atan2(y, x) で角度を求める */
.arrow {
--dx: 100;
--dy: -50;
/* atan2()の戻り値はラジアン角度 */
transform: rotate(atan2(var(--dy), var(--dx)));
}atan2(y, x) の形で引数を渡します。戻り値は角度(ラジアン)なので、rotate() にそのまま渡せます。
たとえば、矢印を特定の方向に向けたいときや、2点間の角度を計算したいときに便利ですね。カスタムプロパティと組み合わせれば、JavaScriptから座標値だけ渡して角度計算はCSS側で行う、なんてことも可能です。
/* マウス座標に向かって回転する要素(JSと併用) */
.pointer {
--mouse-x: 0;
--mouse-y: 0;
--center-x: 150;
--center-y: 150;
transform: rotate(
atan2(
calc(var(--mouse-y) - var(--center-y)),
calc(var(--mouse-x) - var(--center-x))
)
);
}このようにJavaScript側で --mouse-x と --mouse-y を更新するだけで、CSSが角度計算を自動でやってくれます。
ブラウザ対応状況(2026年時点)
2026年3月時点でのブラウザ対応状況をまとめてみました。
- Chrome: 111以降で対応
- Firefox: 108以降で対応
- Safari: 15.4以降で対応
- Edge: 111以降で対応
主要なモダンブラウザはすべて対応済みです。2026年現在ではほぼ気にせず使えるレベルですね。IEはもちろん非対応ですが、IEのサポートが必要なケースはもうほとんどないと思います。
ただし、@property によるカスタムプロパティのアニメーションについては、Firefoxは比較的新しいバージョン(128以降)で対応しているので、円軌道アニメーションなどを使う場合はそこだけ注意が必要です。
JavaScriptとの使い分け
CSSの三角関数とJavaScriptのMath.sin()などをどう使い分けるか、自分なりに整理してみました。
CSSが向いているケース
- 静的な円形配置(時計の文字盤、円形メニューなど)
- CSSアニメーションと組み合わせた軌道運動
- レスポンシブ対応が必要な場合(calc()で柔軟に計算できる)
- JSを減らしてパフォーマンスを上げたい場合
JavaScriptが向いているケース
- 大量の要素を動的に生成する場合
- ユーザー操作に応じたリアルタイムな計算
- 複雑な数学的処理が必要な場合(二重ループなど)
- Canvasでの描画
基本的には「配置やアニメーションに関する計算はCSS」「ロジックや動的生成はJS」という切り分けが良さそうです。両方を組み合わせて、JSからカスタムプロパティの値だけ更新する方法もおすすめですね。先ほどの atan2() の例がまさにそのパターンです。
まとめ
CSSの三角関数を使っていろいろ作ってみましたが、思っていた以上にシンプルに書けて驚きました。特にカスタムプロパティと @property を組み合わせると、かなり柔軟な表現ができますね。
ポイントをまとめるとこんな感じです。
sin()とcos()で円形配置・円軌道アニメーションが作れる@propertyでカスタムプロパティを型付けすればアニメーションが効くatan2()で座標から角度を計算できる- 2026年時点で主要ブラウザはすべて対応済み
- JSと適切に使い分けるのが大事
今回は以上です!