【JavaScript】 Dynamic viewport (lvh, svh, dvh)が効かない時は、Visual viewportを使おう

## はじめに
W3Cの[CSS Values and Units Module Level 4](https://www.w3.org/TR/css-values-4/#viewport-relative-lengths)で、ビューポート単位についていくつか仕様が変更され、ビューポート単位に`Large viewport`、`Small viewport`、`Dynamic viewport`といったビューポートサイズの単位が追加されました。

この新しいビューポート単位は、ほとんどのブラウザの最新バージョンでは使えるようになりましたが、
[Can I use](https://caniuse.com/?search=dynamic%20viewport) でどのくらいのユーザーが対応しているかを確認してみると、2023年2月1日時点で、22%のユーザーは、ちゃんと表示されない状況になっています。

どのくらいまで許容するかは、ドメインごとに違うとは思いますが、実業務で使うには、悩ましいくらいの数値だと思います。

そのため、この記事では、まだ新しいビューポート単位が使えない人に向けてた記事になります。


## 新しいビューポート単位
### 新しいビューポート単位について
まずは新しいビューポート単位について解説します

以前のiOS(Safari)では、画面の高さいっぱいにようを表示したいという時に`100vh`で指定すると、アドレスバーやキーボードが表示された時、下がその分隠れて表示されてしまい、高さいっぱいにならないという現象が起きていました。

その現象を解決するために新しいビューポート単位(`Large viewport`、`Small viewport`、`Dynamic viewport`)は追加されました。

- **Large viewport**（`lvw`・`lvh`・`lvmin`・`lvmax`）
    - Large viewportは、動的に変化するコンテンツ(アドレスバーなど)が格納された時のビューポートサイズが基準のビューポート単位。
- **Small viewport**（`svw`・`svh`・`svmin`・`svmax`）
    - Small Viewportは、動的に変化するコンテンツ(アドレスバーなど)が展開された時のビューポートサイズが基準のビューポート単位。
- **Dynamic viewport**（`dvw`・`dvh`・`dvmin`・`dvmax`）
    - Dynamic Viewportは、動的に変化するコンテンツ(アドレスバーなど)が、格納された時、展開された時を考慮して、ビューポートサイズが変化するビューポート単位。


### ブラウザの互換性
*(2023年2月1日時点のスクリーンショット)*
![スクリーンショット 2023-01-31 22.57.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/2a9a6530-0ae4-0223-0f70-a48cc7e866ba.png)


最新状態は↓こちらをごらんください。

https://caniuse.com/?search=dynamic%20viewport


## 新しいビューポート単位が使えない時
`Large viewport`、`Small viewport`、`Dynamic viewport`が使えない時は、Visual viewportを使うと似たような動きになるので紹介します。

### Visual viewportとは？
Visual viewportは、`window`インターフェースの 読み取り専用のプロパティで、以下のプロパティからビジュアル ビューポートのサイズを読み取ることができです。

+ `VisualViewport.offsetLeft`
    + 左端からビジュアル ビューポートの左端までのオフセットをCSSピクセル単位で返します。
+ `VisualViewport.offsetTop`
    + 上端からビジュアル ビューポートの上端までのオフセットをCSSピクセル単位で返します。
+ `VisualViewport.pageLeft`
    + 上端の最初の包含ブロックの原点に対するビジュアル ビューポートのx座標をCSSピクセル単位で返します。
+ `VisualViewport.pageTop`
    + 上端の最初の包含ブロックの原点に対するビジュアル ビューポートのy座標をCSSピクセル単位で返します。
+ `VisualViewport.width`
    + ビジュアル ビューポートの幅を CSS ピクセル単位で返します。
+ `VisualViewport.height`
    + ビジュアル ビューポートの高さを CSS ピクセル単位で返します。
+ `VisualViewport.scale`
    + ビジュアル ビューポートに適用されるピンチ ズーム倍率を返します。

詳しくは、↓こちらをごらんください。

https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport

### ブラウザの互換性

*(2023年2月1日時点のスクリーンショット)*
![スクリーンショット 2023-01-31 23.51.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/6c7b65d1-6d54-4ad3-f3f7-8f8a561d3913.png)

最新状態は↓こちらをごらんください。

https://caniuse.com/?search=visualViewport

## 実際動いている様子もご覧ください。

![シーケンス 01_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/c5c910db-bc7e-649c-9910-0ae7f40a8d2f.gif)


## サンプル

以下はviewportに合わせて、divのheightを変えるサンプルです。
codepenで実際にお試しください。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="OJwRYjM" data-editable="true" data-user="sp_degu" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/OJwRYjM">
  Visual viewport</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

**① viewportの高さを取得する**
```js
const height = window.visualViewport.height;
````

↑ この部分でviewportの高さを定義しています。

**② viewportの高さをdivに反映する**
```js
container.style.setProperty('--height', `${height}px`)
```

↑ この部分でcssのカスタムプロパティに値を反映させています。

**③ リサイズに合わせて、サイズを変更する**
```js
window.visualViewport.addEventListener('resize', resizeHandler);
```
リサイズされた時に高さを変更するようにイベントを取得しています。


## まとめ
この記事では、新しいビューポート単位が使えない人に向けて、新しいビューポート単位を使わずにviewportサイズを使う方法を紹介しました。

`Large viewport`、`Small viewport`、`Dynamic viewport`は、あと1~2年ほどすれば迷わず使うことができるくらいにはなると思いますので、少し我慢しましょう。

---

最後まで読んでくださってありがとうございます！

普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。

https://twitter.com/degudegu2510