基準幅で作ったデザインを画面幅が変わっても保ちたい!
さいしょに
デザインには、それを組んだときの画面幅があります。これを基準幅と呼びます。本文ブロックの幅と文字サイズの比は、基準幅の上で決まっています。基準幅より狭い画面ではブロックの幅だけが縮み、文字サイズはpxで書いた値のまま残ります。すると1行に入る文字数が減って、改行位置がずれます。
1行の文字数が変わると、行数も変わります。行数が変わると縦に伸びるので、1画面に収める設計だったのに崩れちゃったとかが起こりますね。改行位置に意味がある場合はなおさらで、句読点の直後で切りたかった行が、途中で折り返されてしまいます。
崩れる原因は幅と文字サイズの比が変わることなので、比を保てば直ります。つまり幅がn倍になるなら、文字サイズもn倍にすればよい。
1pxの長さをこちらで決める
CSS変数に長さを入れておき、寸法はその倍数で書きます。たとえば--sp-unitが0.82pxなら、calc(14 * var(--sp-unit))は11.49pxになります。文字も幅も余白も同じ変数を掛けるので、比は絶対に変わりません。
単位を配る
幅は指定値ではなく実測値から取ります。100vwはスクロールバーの幅を含むので、縦スクロールのあるページでは数pxずれます。
const applyUnit = (el, designWidth = 390) => {
// 基準幅より広い画面では等倍のまま。拡大はしない
const scale = Math.min(1, el.clientWidth / designWidth)
el.style.setProperty('--sp-unit', `${scale}px`)
}
寸法を倍数で書く
.page {
/* 変数が来る前でも壊れないようフォールバックを置く */
--u: var(--sp-unit, 1px);
padding: 0 calc(16 * var(--u)) calc(16 * var(--u));
}
.page__body {
width: calc(300 * var(--u));
font-size: calc(14 * var(--u));
line-height: 2;
/* pxではなくem。フォントサイズに追従する */
letter-spacing: 0.05em;
}
--uという短い別名を置くと、calc(14 * var(--u))が14デザインpxとして読めます。
composableにまとめる
Vueなら、幅を測ってstyleに流し込むcomposableにしておくと、ページ側は変数を受け取るだけになります。
import { computed, onBeforeUnmount, onMounted, ref, type ShallowRef } from 'vue'
/** 実測幅と基準幅の比からデザイン座標系の1pxを決める */
export function useSpUnit(target: Readonly<ShallowRef<HTMLElement | null>>, designWidth: number) {
const scale = ref(1)
let observer: ResizeObserver | null = null
const update = (host: HTMLElement) => {
if (host.clientWidth > 0) scale.value = Math.min(1, host.clientWidth / designWidth)
}
onMounted(() => {
// 測るのは自分ではなく親。自分を測ると値が幅を変える循環になる
const host = target.value?.parentElement
if (!host) return
update(host)
if (typeof ResizeObserver !== 'undefined') {
observer = new ResizeObserver(() => update(host))
observer.observe(host)
}
})
onBeforeUnmount(() => observer?.disconnect())
return computed(() => ({ '--sp-unit': `${scale.value}px` }))
}
<script setup lang="ts">
const rootEl = useTemplateRef('rootEl')
const unitStyle = useSpUnit(rootEl, 390)
</script>
<template>
<div ref="rootEl" class="page" :style="unitStyle">...</div>
</template>
親要素を測るのは、自分の幅が--sp-unitに依存しているからです。自分を測ると、測り直しが値を変え、変わった値がまた幅を変える循環になります。親要素は画面から幅が決まるので、その循環に入りません。
導入の流れ
ルートに--sp-unitを配る → --u: var(--sp-unit, 1px)で別名を作る → pxで書いた寸法をcalc(N * var(--u))に置き換える。
置き換えるのは、フォントサイズ、テキストブロックの幅、余白、gap、絶対配置の位置です。字間はemへ。aspect-ratioのように比率で書いてあるものは、そのままで動きます。
気をつけるところ
- 字間をpxで書いていると、文字だけ縮んで字間が残り、1行に入る文字数が変わります。emに直せばフォントサイズに追従します。
- 枠線やhairlineは縮めないほうが素直です。1pxを割る線はブラウザによって消えたり濃さが変わったりします。
- 共通コンポーネントは
var(--sp-unit, 1px)のフォールバック付きで書いておくと、変数を配っていないページの見た目が変わりません。 - 画面の高さ基準の値(
svhなど)は縮めません。縮尺の対象は横幅から決まる寸法だけです。 - 小数の寸法が並ぶので、隣り合う要素の境目が1px揺れることがあります。ぴったり合わせたい箇所は同じ計算式から作ります。