JSでCSSカスタム変数を登録―vhの問題を改善

   · ☕ 7 min read
vhがうまくきまらない!

CSS でvhを使用すると、いくつかの問題—アドレスバーとの干渉、ブラウザ別の差異など—が発生し、想定通りの設定とならないことがある。

これは既知の問題であり、Chrome の Ver56 より発生していると言われている。

URL Bar Resizing

vh units will be sized to the “largest possible viewport”. This means 100vh will be larger than the visible height when the URL bar is shown.

この記事では、私がこの問題に直面するまでの経緯と、それを解決するために実行した方法――CSSカスタム変数の登録――について述べる。

モチベーション

今回おこなった具体的な対応は、以下の記事の方法をとったに過ぎない。
The trick to viewport units on mobile

しかしより詳しい実装の詳細を知りたかったため、記事にまとめることとした。

はじめに

ネイティブアプリっぽくしたい

以下のような電卓アプリを作成した。

この時にふと、ネイティブアプリのようなデザインにできないものかと考えた。

具体的にはブラウザのタブひとつ分を電卓画面で覆い、これによりスクロールを発生させないことで、ウェブページではなくネイティブアプリっぽい感じを表現できるのではないかと考えた。

benibana2001/kawaii-calculator

Vue.jsで作った電卓アプリ。ブラウザ全体を電卓で埋めるために、CSSで'height:100vh'を指定している。スクロール可能な領域を排除し、ネイティブアプリっぽい雰囲気を狙った。

vh が狙い通りにいかない

この「ネイティブアプリっぽく見せる目論見」は、コンテンツの高さが全体で100vhとなるようにCSSを設定することで表現した。そしてこれはPC 表示の際はうまくいった(と私は思っている)。しかし iPhone や Android 端末で確認を行うと(PCのDevTool ではなく実機)、なぜか高さが合わなかった。100vhよりも大きい値の高さが発生してしまった。これはそれぞれのブラウザで表示されるアドレスバータブバーの領域が原因なようだ。 アドレスバーやタブバーの領域の高さの分だけ、画面全体の表示領域が増加してしまっている。これが冒頭で述べた、URL Bar Resizing という問題だ。

iOS Chromeで表示した電卓アプリ。左:やりたかったこと。右:実際の結果。
## 改善策 この、スマホの`vh`異常の改善策は、こちらにあった。

The trick to viewport units on mobile

ここでは、この記事の内容を細かく記していく。

TL;DL

はじめに、実際に行った改善の手順を記す。

  1. window.innerHeightを使用し、アドレスバー、タブバーを除外した高さを取得する。
  2. 1 の高さの 1/100 の値を、CSS 変数として登録する。登録にはCSSStyleDeclarationAPI を使用する。
  3. DOM 要素の高さをvhを使用して指定する代わりに、2 で登録した変数を用いて指定する。

これはつまり、vhは当てにならないので、JSを使って正しい値を都度計算し、それをCSSで使用するということである。この方法をとることで、デバイスやブラウザに依存せず、狙った通りの値を設定できる。

以下、詳細を見ていく。

実装1. アドレスバー、タブバーを除外した高さを取得する。

そもそもvhは、layout viewport の 1%を取得できるプロパティだ。

A vh unit is 1% of the layout viewport’s height. …
(https://developer.mozilla.org/en-US/docs/Web/CSS/Viewport_conceptsより)

しかしvhは、ユーザーによるスクロール操作といった何らかの影響により、正しい値とならないことがある。

Usable vh units on mobile. Prior to this, using vh units meant a page would reflow jarringly everytime the user changed scroll direction.
(URL Bar Resizing より)

特にモバイル端末では、アドレスバーなどの影響が大きく、vh の値が狂う可能性が高い。

そのため、あえてvhを使用しない。代わりに、window.innerHeightを使用する。この API は個々の端末やブラウザの種類に影響されず、表示領域のみを正確に算出することが可能だ。そのため不安定なvhを使用することなく、目的とする値を正確に算出するのに役立つ。

innerHeightの説明。アドレスバーやタブバーの高さを差し引いた領域の高さを取得できる。引用:https://developer.mozilla.org/en-US/docs/Web/API/Window/innerHeight

実装2. 1 で求めた値を元に、CSS 変数を登録する。

innerHeightの値を 100 分割することで、表示領域の 1%を取得することができる。この値こそが、我々が本来欲していたvhの値だ。

CSSStyleDeclaration

この100分割した値を CSS で使用・設定できるようにするために、CSSStyleDeclarationインターフェースを使用する。これは「スタイル情報やスタイル関連のメソッド、プロパティを提供するインターフェース」であり、一般的には下記のように取得することができる。

1
const bodyStyleDeclare = document.body.style

このインターフェースは、あらゆるDOM要素が保有している。そしてこのインターフェースが保有するsetPropertyというメソッドを使用し、CSS変数の登録を行う。

CSSStyleDeclaration.setProperty()

このメソッドは、CSSスタイルブロック宣言に、プロパティを設定するものだ。これを使用することで、自由にStyleを読み取り・書き込みすることができる。

例えば、このメソッドを使用するタイミングにもよるが、DOMのレンダリング前にプロパティを設定することで、JSで作成した変数をカスタム変数としてCSSで使用することが可能だ。

CSSStyleDeclaration.setProperty()

あらゆるDOM要素はこのメソッドを保有しているが、ここではdocument要素に対して新たな Style(CSSプロパティ) を追加することとする。

ここまでの操作をコードで表すと、以下のようになる。

1
2
3
4
5
6
7
// 'customVh' という独自変数を作成。
// これはスマホのviewportからアドレスバーやナビバー領域を除外した値の、1/100とする。
const customVh = window.innerHeight * 0.01;

// setPropertyメソッドを使用してStyleを登録
// CSS変数は、頭に'--'を付ける必要がある。
document.documentElement.style.setProperty("--customVh", `${customVh}px`);

実装3. 作成した変数を用いて DOM の高さを指定する。

最後に、上記で定義したカスタム変数をvhの代わりに使用する。

1
2
3
4
5
6
7
8
9
.container {
  /* CSS変数が正しく機能しないときのための保険 */
  height: 100vh;

  /* CSS変数はvarを使用して呼び出す。 */
  /* ここでは、JSで定義した 'customVh'というCSS変数を呼び出している */
  /* var関数の第二引数は、CSS変数が存在しないときに代わりに呼ばれる値を表す */
  height: calc(var(--customVh, 1vh) * 100);
}

CSSでのカスタム変数の使用方法はMDNに丁寧にまとめられています。

おわりに

以上の手順を行うことで想定した通りの viewport の高さ設定が可能となる。

FWやライブラリを使用するときは

Vue.jsやReactのようなライブラリ・FWを使用する場合は、CSSファイルで変数が正常に読み込まれるようにするために、DOMのマウントのタイミングでCSSプロパティを設定するとよいと思います。

今回のソースは、benibana2001/kawaii_caluculatorにあります。Vue.jsのVueインスタンスの内部で定義しているため、仮想DOMがマウントされた段階でCSS変数の定義、登録を行なっています。

MDNの日本語訳について

本記事で何度か引用させていただいているMDN web docsですが、翻訳されていない記事がまだまだあります。興味がある方は、翻訳してみることをお勧めします。

私は現在、記事内でも取り上げたCSSStyleDeclarationAPIの翻訳を行っています。

MDNの翻訳のノウハウに関しては、Mozilla Japanが定期的に開催しているミートアップでを学ぶことができるのでお勧めです。

Share on

whasse
WRITTEN BY
whasse
Web Developer