CSSだけ作る、フォームに文字を入力するとレイアウトがぶっ壊れるページ

## この記事の概要

以下の内容、ノリで作ったら思ったよりウケたので記事にします。

https://twitter.com/xrxoxcxox/status/1598289734513889280

次のページから実際に触れます（なお、FirefoxとInternet Explorerでは動かないのでご了承ください）。

https://xrxoxcxox.github.io/qiita-has-trick/

リポジトリも公開しています。

https://github.com/xrxoxcxox/qiita-has-trick

:::note alert
この記事に書いてあるテクニック自体は役に立つと思いますが、内容そのものは何にも役立たないのでご注意ください！
:::

## 何をしているのか

CSSの`:has()`を使って、子要素の操作をキャッチして親要素のスタイルを変えています。
そのため、プレーンなHTMLとCSSだけで冒頭のツイート内動画の内容を実現できました。

ただし、`:has()`を使える環境が割と増えてきたとは言え、Firefoxや少し古いSafariは未対応です。
業務で使うのはまだ時期尚早かもしれないので、そこはご注意ください。

<picture>
<source type="image/webp" srcset="https://caniuse.bitsofco.de/static/v1/css-has-1669900123948.webp">
<source type="image/png" srcset="https://caniuse.bitsofco.de/static/v1/css-has-1669900123948.png">
<img src="https://caniuse.bitsofco.de/static/v1/css-has-1669900123948.jpg" alt="Data on support for the css-has feature across the major browsers from caniuse.com">
</picture>

どうしても使いたい場合、ポリフィルに頼るのがベターでしょうか。
過去に記事を書いているので、興味のある方はこちらもどうぞ！

https://qiita.com/xrxoxcxox/items/74c2443026de8b10a92c

## 実装内容と解説

今回の内容のコアに関わる部分を抜粋して解説します。

```html:HTML
<!-- headなどは省略 -->
<body>
  <div class="container">
    <header class="header">
      <span class="logotype">Dummmmy</span>
    </header>
    <main class="main">
      <div class="panel">
        <h1 class="title">ぶっ壊れるページ</h1>
        <input type="text" placeholder="文字を入力しないでください" class="input">
      </div>
    </main>
    <footer class="footer">
      <div class="footer-inner">
        <span>Dummmmy</span>
        <span class="copyright">© 2022 Dummmmy Inc.</span>
      </div>
    </footer>
  </div>
</body>
```

```css:CSS
:root {
  --header-height: 64px;
  --main-height: calc(100vh - var(--header-height) - var(--footer-height));
  --footer-height: 240px;
}

.container {
  position: relative;
}

.header {
  height: var(--header-height);
  position: relative;
  transform-origin: top left;
  z-index: 1;
}

.main {
  height: var(--main-height);
  position: relative;
}

.panel {
  position: relative;
}

.title {
  position: relative;
}

.input {
  position: relative;
}

.footer {
  height: var(--footer-height);
  position: relative;
}

@keyframes fall-header {
  0% {
    left: 0;
    rotate: 0;
    top: 0;
  }
  /* 省略 */
  100% {
    left: 2vw;
    rotate: 12deg;
    top: calc(var(--main-height));
  }
}

.container:has(.input:not(:placeholder-shown)) .header {
  animation: fall-header 600ms ease-in-out 0ms 1 forwards;
}

@keyframes fall-panel {
  0% {
    left: 0;
    rotate: 0;
    top: var(--header-height);
  }
  /* 省略 */
  100% {
    left: -2vw;
    rotate: 3deg;
    top: calc(2 * var(--header-height));
  }
}

.container:has(.input:not(:placeholder-shown)) .panel {
  animation: fall-panel 300ms ease-in-out 400ms 1 forwards;
}

@keyframes fall-title {
  0% {
    left: 0;
    rotate: 0;
  }
  100% {
    left: 1vw;
    rotate: 3deg;
  }
}

.container:has(.input:not(:placeholder-shown)) .title {
  animation: fall-title 350ms ease-in-out 450ms 1 forwards;
}

@keyframes fall-input {
  0% {
    left: 0;
    rotate: 0;
    top: 0;
  }
  100% {
    left: -5vw;
    rotate: -5deg;
    top: 50px;
  }
}

.container:has(.input:not(:placeholder-shown)) .input {
  animation: fall-input 200ms ease-in-out 450ms 1 forwards;
}

@keyframes fall-footer {
  0% {
    left: 0;
    rotate: 0;
    top: 0;
  }
  /* 省略 */
  100% {
    left: 1vw;
    rotate: -2deg;
    top: 80px;
  }
}

.container:has(.input:not(:placeholder-shown)) .footer {
  animation: fall-footer 400ms ease-in-out 200ms 1 forwards;
}
```

### 0. CSSカスタムプロパティで高さを再利用

この内容は、単に少し効率が良くなるだけです。
CSSの冒頭で`:root`の中で`header`と`main`と`footer`の高さをカスタムプロパティとして登録しています。
これにより「要素の高さ分だけアニメーションさせる」のが容易になります。

### 1. 動かしたい要素をすべて`position: relative;`で配置する

とんでもないコードです。
実務でこんなコードを書いたらNGですが、今回はギミック優先です。

`.container`や`.header`など、動かしたい要素はすべて`position: relative;`です。

### 2. `:has`を使う

今回の記事のメインです。

CSS内で何箇所か`.container:has(.input:not(:placeholder-shown)) .header {}`といった記述をしています。
（`.header`の部分は`.panel`だったり`.title`だったりしますが、どこも仕組みは一緒です）

個別に和訳（？）すると以下のようになります。

1. .container:has()
    - 「`.container`の子要素が`:has()`の`()`内にマッチしたら〜」
1. .input:not()
    - 「`.input`のうち`:not()`内にマッチしなかったら〜」
1. :placeholder-shown
    - 「`placeholder`が表示されていたら〜」

全体を通すと「`placeholder`が表示されていない`.input`を子要素に持つ`.container`の子要素である`.header`にマッチしたら〜」となっています。

長々と書きましたが、この指定によって「ページ内に唯一存在する`input`に文字を入力しただけでページ全体のレイアウトが壊れる」を実現しています。

### 3. 気合いでアニメーションを書く

特にライブラリなど使っていないので「物が落下してるっぽいアニメーション」を勘で書きました。
これはこれで、いつか記事にするかもしれません。
今回は省略していますが、リポジトリは公開しているので気になる方はこちらをご覧ください。

https://github.com/xrxoxcxox/qiita-has-trick

## 最後に

冒頭にも書いた通り、今回のコードそのものは実務では全然役立ちません。
しかし「子要素の操作をきっかけに親要素のスタイルを触りたい」場面はごくたまにあります（私だけかもしれませんが……）。
もしそういう場面に遭遇した際、この記事がお役に立てれば幸いです。

---

最後まで読んでくださってありがとうございます！
Twitterでも情報を発信しているので、良かったらフォローお願いします！

https://twitter.com/xrxoxcxox

Devトークでのお話してくださる方も募集中です！

https://jobs.qiita.com/employers/qiita-inc/dev_talks/489