WebGLで作るリキッドメタルボタン|paper-design/shadersでメタリックUIを実装する


リキッドメタルボタンとは、WebGLのフラグメントシェーダーで金属の反射をピクセル単位に計算し、液状の金属のように光沢が流れて見える質感を持たせたボタンUIです。CSSのグラデーションでは止まった金属しか作れませんが、シェーダーを使うと反射そのものが動く「生きた金属面」を表現できます。

メタリックなボタンを作ろうとすると、多くの人はlinear-gradientbox-shadowを重ねます。しかし、それでは「金属っぽい色」は出せても、見る角度で反射が流れる異方性のある質感までは再現できません。本物の金属面は光源と視線の関係でハイライトが連続的に変化するため、計算が必要だからです。

この記事では、paper-design/shadersというオープンソースのシェーダーライブラリを使い、GLSLを一行も書かずにリキッドメタルボタンを実装する方法を解説します。最小コード・主要パラメータの早見表・パラメータ違いの比較・CSSで「ボタン」に仕上げるテクニックまで、そのまま再現できる形でまとめました。WebGLの専門知識がなくても進められます。


完成デモ:6パターンのリキッドメタルボタン

まずは完成形を見てください。下のデモは、反射の細かさ(repetition)と流れる角度(angle)を変えた6種類のボタンを並べたものです。同じシェーダーでも、パラメータ次第でヘアライン仕上げから水銀のような粗い反射まで質感が大きく変わります。

差分チェックツールで効率UPお手本コードと自分のコードを比較して、違いを一目で確認できます。練習前にブックマークしておくと便利です。
Diff Checkerを開く →

このボタンはシェーダーが描く金属面CSSで重ねた暗い中央面・リム(縁)の二層構造でできています。次の章から、その作り方を順番に分解していきます。


なぜCSSではなくWebGLシェーダーなのか

結論から言うと、流動する金属の反射はCSS単体では実用的に再現できません。CSSのグラデーションは「色の帯」を静的に並べるだけで、ピクセルごとに反射方向を計算する仕組みを持たないからです。

表現したいことCSSグラデーションWebGLシェーダー
金属っぽい色得意得意
連続的に流れる反射困難(帯が固定される)得意(ピクセル単位で計算)
色収差(赤・青のにじみ)ほぼ不可パラメータで調整可能
視線・角度による変化不可得意
導入の手軽さ非常に手軽ライブラリで手軽

ただし、シェーダーを自分でGLSLから書くのは学習コストが高いのも事実です。そこで本記事では、金属反射のシェーダーがあらかじめ用意されたpaper-design/shadersを使います。これはnpmで公開されているオープンソースのシェーダーライブラリで、JavaScriptからパラメータを渡すだけで質感を調整できます。


最小実装:ShaderMountでシェーダーをマウントする

paper-design/shadersの基本は、対象のDOM要素にShaderMountを生成し、シェーダーとパラメータ(uniform)を渡すだけです。まずはHTMLの土台から見ていきます。

<div class="metal-button">
  <div class="outline"><span class="label">BUTTON</span></div>
</div>

次に、この.metal-buttonにシェーダーをマウントします。type="module"のスクリプトとして読み込むのがポイントです。

import {
  liquidMetalFragmentShader,
  ShaderMount
} from "https://esm.sh/@paper-design/shaders@0.0.76";

const el = document.querySelector(".metal-button");

new ShaderMount(el, liquidMetalFragmentShader, {
  u_colorBack: "#000000",   // 金属の影側の色
  u_colorTint: "#ffffff",   // 反射ハイライトの色
  u_repetition: 0.4,        // 反射の縞の細かさ
  u_softness: 0.45,         // 反射エッジの柔らかさ
  u_shiftRed: 0.2,          // 赤の色収差
  u_shiftBlue: 0.2,         // 青の色収差
  u_angle: 0,               // 反射が流れる角度(度)
  u_scale: 1.5,             // 模様全体の拡大率
  u_offsetX: 0.1,           // 反射中心の横ずらし
  u_offsetY: -0.1           // 反射中心の縦ずらし
});

これだけで、.metal-buttonの中に金属反射を描くcanvasが生成されます。GLSLは一行も書いていません。liquidMetalFragmentShaderがシェーダー本体で、第3引数のオブジェクトがその挙動を決めるパラメータ群です。


主要パラメータ(uniform)早見表

リキッドメタルの質感は、次のパラメータの組み合わせで決まります。まずはこの表を見ながら、いくつか値を動かして反応を確かめるのが理解の近道です。

パラメータ役割デモでの値
u_colorBack背景=金属の影側の色#000000
u_colorTint反射ハイライトの色#ffffff
u_repetition反射の縞の繰り返し数。大きいほど細かい0.2〜1.5
u_softness反射エッジの柔らかさ。大きいほどぼやける0.45
u_shiftRed / u_shiftBlue赤・青の色収差(プリズムのにじみ)0.2
u_angle反射が流れる角度(度)0〜90
u_scale模様全体の拡大率1.5
u_offsetX / u_offsetY反射中心の位置ずらし0.1 / -0.1
u_distortion反射の歪みの強さ0
u_contour輪郭の強調0

もっとも質感を左右するのはu_repetitionu_angleの2つです。次の章で、この2つを変えた6パターンを具体的に比較します。


6パターン比較:repetition×angleで質感はこう変わる

冒頭デモの6つのボタンは、それぞれ次の設定になっています。値と見え方の対応を押さえると、狙った質感に最短で近づけます。

ラベルrepetitionangle見え方
A0.40横方向に反射が流れる標準的なメタル。縞は粗め
B0.80縞が細かくなり、ヘアライン仕上げ風の上品な質感
C0.445斜めに反射が流れ、動きを感じる質感
D0.845斜め+細かい縞。研磨された金属に近い
E1.590縦方向に非常に細かい縞。光沢が強くシャープ
F0.20縞が最も粗く、水銀のようにゆったり流れる反射

傾向はシンプルです。repetitionを上げるほど縞が細かく金属が硬質になり、angleを傾けるほど反射の流れる向きが変わって動きが出ます。UIのトーンが落ち着いた製品ならB・D、インパクト重視ならE・Fが向いています。

複数のボタンに別々の値を割り当てたいときは、HTMLのdata-*属性に値を持たせ、JS側で読み取ると管理が楽になります。

document.querySelectorAll(".metal-button").forEach((el) => {
  new ShaderMount(el, liquidMetalFragmentShader, {
    u_colorBack: "#000000",
    u_colorTint: "#ffffff",
    u_repetition: parseFloat(el.dataset.rep), // data-rep から取得
    u_angle: parseFloat(el.dataset.ang),      // data-ang から取得
    u_softness: 0.45,
    u_scale: 1.5
  });
});

CSSで「ボタン」に仕上げる:リムと暗い中央面

シェーダーは要素全体を金属面で塗りつぶします。そのままでは「金属の板」にしか見えないため、CSSで中央を暗い面で覆い、縁のリム(ベゼル)だけ金属を見せるのがこのデモの肝です。これでボタンらしい立体感が生まれます。

:root {
  --radius: 3rem;   /* 角丸 */
  --rim: 0.3rem;    /* リム(ベゼル)の太さ */
}

.metal-button {
  position: relative;
  width: 30rem;
  height: 10rem;
  border-radius: var(--radius);
  overflow: hidden;            /* シェーダーcanvasを角丸でクリップ */
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

/* 中央を暗い面で覆い、メタルを縁のリムだけ見せる */
.metal-button::before {
  content: "";
  position: absolute;
  inset: var(--rim);
  background: linear-gradient(#444, #000);
  border-radius: calc(var(--radius) - var(--rim));
  box-shadow: inset 0 0.2rem 0.2rem 0.2rem rgba(255, 255, 255, 0.3);
  z-index: 1;
}

/* ラベルは canvas の上(z-index: 2)に通常のDOMで重ねる */
.outline {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 2;
}

.label {
  font-size: 2.6rem;
  font-weight: 700;
  letter-spacing: 0.25em;
  color: #65615f;
}

ポイントは3つです。overflow: hiddenでcanvasを角丸にクリップし、::beforeで中央を暗くしてリムを残し、ラベルはz-index: 2でcanvasの上に重ねます。ラベルをcanvasの外(通常のDOM)に置くことは、後述するアクセシビリティの観点でも重要です。


クリック時の沈み込みを足す

金属面が動いているだけでは「押せる」感触が伝わりません。:activeでわずかに沈み込ませると、物理ボタンのような操作感が加わります。

.metal-button:active {
  transform: translateY(0.2rem) scale(0.98);
  box-shadow: 0 0.3rem 0.8rem rgba(0, 0, 0, 0.5);
}

このように、シェーダーは質感を、CSSは構造とマイクロインタラクションを担当する、という役割分担で考えると設計しやすくなります。スクロール連動やトランジションなど他の動きと組み合わせる発想は、Webアニメーション完全ガイドも参考になります。


パフォーマンスと使いどころの注意点

リキッドメタルボタンはGPUで描画されるためCPU負荷は小さい一方、各ボタンが常時アニメーションするcanvasになります。乱用するとモバイル端末でのGPU負荷とバッテリー消費が増えるため、使う場所を絞るのが鉄則です。

  • 主役のCTAに1〜数個まで: 全ボタンを金属化すると視覚的に騒がしく、負荷も上がる。ヒーローの主CTAなど見せ場に絞る
  • 画面外では止める: 多数並べる場合はIntersectionObserverで画面内のものだけ描画すると負荷を抑えられる
  • prefers-reduced-motionを尊重する: 動きを抑える設定のユーザーには静止状態へ切り替える(次章のFAQ参照)
  • バージョンを固定する: esm.shのURLでバージョン(@0.0.76)を固定し、更新で表示が変わらないようにする

同じWebGLでも、背景全体に屈折ガラスを敷くiOS風Liquid Glassの再現や、マウス追従のWebGL流体エフェクトとは負荷特性が異なります。表現ごとに「どこに何個置くか」を設計することが、UIモーションを破綻させないコツです。


うまく表示されないときのチェックリスト

実装して金属が出ない・角が四角いといった症状は、ほとんどが次のいずれかです。上から順に確認してください。

  • 何も描かれない: マウント先の要素に幅・高さがあるか確認する。display: noneや高さ0の要素にはcanvasが描画されない
  • importでエラー: スクリプトをtype="module"で読み込んでいるか確認する。通常のscriptタグではESMのimportが動かない
  • 角が四角いまま: 親要素のoverflow: hiddenが抜けている。canvasは矩形なので角丸クリップが必要
  • 中央の暗い面が出ない: ::beforez-indexとcanvasの重なりを確認する。canvasがリムまで覆い隠していないか見る
  • ラベルが見えない: ラベルのz-indexがcanvasより下になっている。ラベルは最前面(例:z-index: 2)に置く

ここまで動けば、あとはパラメータを変えて自分の製品トーンに合う金属を探すだけです。冒頭デモのA〜Fを出発点に、repetitionとangleから触ってみてください。


よくある質問(FAQ)

Q. リキッドメタルボタンはCSSだけで作れますか?

反射が連続的に流れる本格的な質感は、CSS単体では実用的に再現できません。linear-gradientとアニメーションで近似はできますが、ピクセル単位で反射を計算するWebGLシェーダーには質感で及びません。手軽に作りたい場合は、本記事のようにシェーダーライブラリを使うのが現実的です。

Q. WebGLやGLSLの知識は必要ですか?

不要です。paper-design/shadersではliquidMetalFragmentShaderというシェーダー本体がライブラリ側に用意されており、ShaderMountにパラメータを渡すだけで質感を調整できます。GLSLを書くのは、独自のシェーダーを自作したくなった段階で十分です。

Q. パフォーマンスへの影響はどのくらいですか?

描画はGPUが担うためCPU負荷は小さいですが、ボタンごとに常時更新されるcanvasが増える点に注意が必要です。1画面に多数並べるとモバイル端末でフレームレート低下やバッテリー消費の増加につながります。主役のCTAなど1〜数個に絞り、画面外では描画を止めるのが安全です。

Q. アクセシビリティで気をつけることは?

常時動く演出は、prefers-reduced-motionの指定があるユーザーには静止状態へ切り替えるべきです。MDNのprefers-reduced-motionを参照し、メディアクエリでアニメーションを抑制してください。また、ラベル文字はcanvas内ではなく通常のDOM要素で置き、十分なコントラストを確保することが重要です。

Q. React や Next.js でも使えますか?

使えます。useEffectの中でマウント先のDOM(refで取得)にShaderMountを生成し、クリーンアップ関数で破棄する形にすれば、コンポーネントの再レンダリングでも安全に動きます。npmから@paper-design/shadersを導入すれば、esm.sh経由ではなくバンドルに含めて配信できます。


まとめ

  • 流動する金属反射はWebGLシェーダーで作る。CSSグラデーションでは止まった金属しか出せない
  • paper-design/shadersを使えば、GLSLを書かずにShaderMount+パラメータだけで実装できる
  • 質感はu_repetition(縞の細かさ)u_angle(流れる角度)でほぼ決まる
  • シェーダーは質感、CSSは構造(リム・暗い中央面)とクリック挙動を担当させると設計しやすい
  • 常時描画されるため、主役のCTAに絞り、prefers-reduced-motionを尊重する

シェーダーは「難しそう」で敬遠されがちですが、ライブラリを使えばパラメータ調整だけで一段上の質感が手に入ります。まずは冒頭デモのコードをそのまま動かし、自分の製品に合う金属を探してみてください。

「自社サイトのCTAやブランドサイトにこうした表現を組み込みたい」という場合は、実装・制作の相談を承っています。


関連記事

iOS風「Liquid Glass」をWebで再現する方法

マウスストーカー炎アニメーション|WebGLで作る流体エフェクト「火華」

Webアニメーション完全ガイド|Animate.css・AOS・IO・GSAP 4手法を比較解説