🍎

AppleのLiquid GlassをSvelteで再現してみた

に公開

こんにちは。

AppleのWWDC25で発表された新しいUI、Liquid Glassにびっくりしました。

https://www.youtube.com/watch?t=601&v=0_DjDdfqtUE

動画にある通り、ガラスのような見た目のボタンやアイコンを新たなデザインとして発表しました。

これを見た途端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>

見た目はこのような感じです。

Liquid Glass Example

詳しくはnpmのページを参照してください。
https://www.npmjs.com/package/liquid-glass-svelte

再現する

まずはじっくり観察することから始めます。

Liquid Glass
https://youtu.be/0_DjDdfqtUE?t=2998より

まずわかるのはぼかしの強度が場所によって異なっているということです。

中央と比べ、周囲はぼかしが強いことがわかります。

また、縁のあたりが光っていることがわかります。

これらを踏まえCSSで再現していきます。


完成したコードはGitHubに公開されていますが、中身をかるくご紹介します。

https://github.com/Tozaburo/liquid-glass-svelte

まず、コンポーネントが受け取るパラメータは以下の通りです。

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;
}

それぞれ次の数値に対応しています。
Liquid Glass Options

次に、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};`

nonEdgeGradientWidthmainBluredgeBlurの大小関係から求まります。

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を使って透明な部分と黒い部分を作り、ぼかしを適用しています。

欠点として、角のあたりの光沢がうまく行っていません。

Liquid Glass Example

.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で公開しました。

https://www.npmjs.com/package/liquid-glass-svelte

また、GitHubでも公開しています。

https://github.com/Tozaburo/liquid-glass-svelte

npmで公開する方法は後々記事にしようと思います。

Discussion