AppleのLiquid GlassをSvelteで再現してみた
こんにちは。
AppleのWWDC25で発表された新しいUI、Liquid Glassにびっくりしました。
動画にある通り、ガラスのような見た目のボタンやアイコンを新たなデザインとして発表しました。
これを見た途端CSSで再現したいと思いました。
ということで、Liquid GlassをCSSで再現してSvelteコンポーネントにしてnpmで公開するまでです。
使い方
本題の前に、使い方を説明します。
Svelteの環境をViteなどで構築した後、以下のコマンドでインストールします。
npm i liquid-glass-svelte
その後、以下のようにコンポーネントを使います。
<script lang="ts">
import { LiquidGlass } from "liquid-glass-svelte";
</script>
<LiquidGlass>
<h1>Content goes here</h1>
</LiquidGlass>
見た目はこのような感じです。
詳しくはnpmのページを参照してください。
再現する
まずはじっくり観察することから始めます。
https://youtu.be/0_DjDdfqtUE?t=2998より
まずわかるのはぼかしの強度が場所によって異なっているということです。
中央と比べ、周囲はぼかしが強いことがわかります。
また、縁のあたりが光っていることがわかります。
これらを踏まえCSSで再現していきます。
完成したコードはGitHubに公開されていますが、中身をかるくご紹介します。
まず、コンポーネントが受け取るパラメータは以下の通りです。
let { children, options: rawOptions = {}, ...pureRest }: LiquidGlassProps = $props();
options
は以下のパラメータがあります。
interface LiquidGlassOptions {
mainBackgroundColor?: string;
mainBlur?: CSSLength;
edgeBlur?: CSSLength;
edgeBackgroundColor?: string;
edgeWidth?: CSSLength;
edgeGradientWidth?: CSSLength;
sheenBlur?: CSSLength;
sheenBackgroundColor?: string;
sheenWidth?: CSSLength;
}
それぞれ次の数値に対応しています。
次に、CSSの変数を設定します。
`--main-background-color:${options.mainBackgroundColor};
--main-blur:${options.mainBlur};
--edge-blur:${options.edgeBlur};
--edge-background-color:${options.edgeBackgroundColor};
--edge-width:${options.edgeWidth};
--edge-gradient-width:${options.edgeGradientWidth};
--non-edge-gradient-width:${options.nonEdgeGradientWidth};
--non-edge-width:calc(${options.nonEdgeWidth});
--sheen-blur:${options.sheenBlur};
--sheen-background-color:${options.sheenBackgroundColor};
--sheen-width:${options.sheenWidth};
--edge-z-index:${options.edgeZIndex};
--non-edge-z-index:${options.nonEdgeZIndex};`
nonEdgeGradientWidth
はmainBlur
とedgeBlur
の大小関係から求まります。
const mainBlurPx = toPx(options.mainBlur) ?? 0;
const edgeBlurPx = toPx(options.edgeBlur) ?? 0;
console.log(`mainBlurPx: ${mainBlurPx}, edgeBlurPx: ${edgeBlurPx}`);
if (mainBlurPx > edgeBlurPx) {
options.nonEdgeGradientWidth = options.edgeGradientWidth;
options.edgeGradientWidth = '0.0001px';
options.edgeWidth = `calc(${options.edgeWidth} + ${options.mainBlur})`;
options.edgeZIndex = -1;
options.nonEdgeZIndex = 0;
} else {
options.nonEdgeGradientWidth = '0.0001px';
options.edgeZIndex = 0;
options.nonEdgeZIndex = -1;
}
HTMLは次のようになっています。
<div class={`glass ${className || ''}`} style={`${cssVars} ${style || ''}`} {...rest}>
<div class="glass-non-edge">
<div class="glass-edge">
<div class="glass-sheen">
{@render children?.()}
</div>
</div>
</div>
</div>
div要素を3つ重ねて、CSSでそれぞれのスタイルを設定しています。
全体のCSSは以下のようになります。
.glass {
position: relative;
overflow: hidden;
z-index: 1;
inset: 0;
border-radius: inherit;
background-color: var(--main-background-color);
& *,
& *::before {
border-radius: inherit;
}
:global(.glass-sheen > *) {
position: relative;
z-index: 1;
}
.glass-sheen::before {
/* 後述 */
}
.glass-non-edge::before {
/* 後述 */
}
.glass-edge:before {
/* 後述 */
}
}
.glass-sheen
は光沢の部分を表現しています。
.glass-sheen::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
backdrop-filter: blur(var(--sheen-blur));
background-color: var(--sheen-background-color);
pointer-events: none;
z-index: 0;
-webkit-mask-image:
linear-gradient(0deg, #000, transparent var(--sheen-width)),
linear-gradient(180deg, #000, transparent var(--sheen-width)),
linear-gradient(90deg, #000, transparent var(--sheen-width)),
linear-gradient(270deg, #000, transparent var(--sheen-width));
mask-image:
linear-gradient(0deg, #000, transparent var(--sheen-width)),
linear-gradient(180deg, #000, transparent var(--sheen-width)),
linear-gradient(90deg, #000, transparent var(--sheen-width)),
linear-gradient(270deg, #000, transparent var(--sheen-width));
mask-composite: add;
mask-type: luminance;
pointer-events: none;
}
グラデーションを使って光沢の部分を表現しています。
光沢の部分は、mask-image
を使って透明な部分と黒い部分を作り、ぼかしを適用しています。
欠点として、角のあたりの光沢がうまく行っていません。
.glass-non-edge
は中央の部分を表現しています。
.glass-non-edge::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
backdrop-filter: blur(var(--main-blur));
pointer-events: none;
z-index: var(--non-edge-z-index);
--gradient:
transparent var(--non-edge-width),
#000 calc(var(--non-edge-width) + var(--non-edge-gradient-width));
-webkit-mask-image:
linear-gradient(0deg, var(--gradient)), linear-gradient(180deg, var(--gradient)),
linear-gradient(90deg, var(--gradient)), linear-gradient(270deg, var(--gradient));
mask-image:
linear-gradient(0deg, var(--gradient)), linear-gradient(180deg, var(--gradient)),
linear-gradient(90deg, var(--gradient)), linear-gradient(270deg, var(--gradient));
mask-composite: intersect;
pointer-events: none;
}
中央の部分は、mask-image
を使って透明な部分と黒い部分を作り、ぼかしを適用しています。
.glass-edge
は縁の部分を表現しています。
.glass-edge:before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: var(--edge-z-index);
background-color: var(--edge-background-color);
backdrop-filter: blur(var(--edge-blur));
--gradient:
#000 var(--edge-width), transparent calc(var(--edge-width) + var(--edge-gradient-width));
-webkit-mask-image:
linear-gradient(0deg, var(--gradient)), linear-gradient(180deg, var(--gradient)),
linear-gradient(90deg, var(--gradient)), linear-gradient(270deg, var(--gradient));
mask-image:
linear-gradient(0deg, var(--gradient)), linear-gradient(180deg, var(--gradient)),
linear-gradient(90deg, var(--gradient)), linear-gradient(270deg, var(--gradient));
mask-composite: add;
mask-type: luminance;
pointer-events: none;
}
自由ですね。
公開する
完成したパッケージはnpmで公開しました。
また、GitHubでも公開しています。
npmで公開する方法は後々記事にしようと思います。
Discussion