posted: 2019/12/04

styled-componentsで作る年末年始にぴったりなCSS Gridにおける要素の重なりの話

この記事は CSS Advent Calendar 2019 4 日目の記事です。
今回は CSS Grid の要素の重なりについて記述します。
なお、styled-components を利用している点についてはご容赦いただければと思います。

CSS Grid の要素を重ねるとどうなるか?

CSS Grid は row / columnで位置を指定する事が出来ます。
const Grid = styled.div`
  display: grid;
  grid-template-rows: repeat(10, 10vmin);
  grid-template-columns: repeat(10, 10vmin);
`

const Pos = styled.div`
  grid-row: ${({ row }) => row};
  grid-column: ${({ col }) => col};
`
位置を指定するということは、当然同じエリアに指定すれば重なることになります。
では重なった場合どうなるでしょう?
const Item1 = styled(Pos)`
  background: blue;
  color: white;
`
const Item2 = styled(Pos)`
  display: grid;
  background: red;
`

const App = () => (
  <Grid>
    <Item2 col="1/3" row="1">
      Item2
    </Item2>
    <Item1 col="1" row="1/3">
      Item1
    </Item1>
  </Grid>
)
stage1-1
はい、赤い要素(Item2)が上に来ました
では逆にすると
const App = () => (
  <Grid>
    <Item1 col="1" row="1/3">
      Item1
    </Item1>
    <Item2 col="1/3" row="1">
      Item2
    </Item2>
  </Grid>
)
stage1-2
青い要素(Item1)が上に来ました。
このように基本的に下に来る要素が上に来る形になります(例外はありますがこれは後述)
この特性を使えば独自定義の座標に基づいて要素を並べて図画を描画出来ます。
すべての対象要素にgrid-rowgrid-columnを付けなければ行けないという欠点がありますが、「px 指定でなく%指定でレスポンシブなものを作れる」「SVG でやりづらかったことを CSS 側で処理できる(割と無いかも?)」などの利点があります。

年末なのでクリスマスツリーを作る

さて、これを使えば CSS でお絵かきが出来ます。
今回はクリスマスツリーを作ってデコレーションしてみましょう。
また、今回せっかく Grid を活かすために、position: absolutepx指定はなるべく縛って行こうと思います。

Grid 基盤

先程のコードでも出ていますが、Grid の基礎部分を先に説明しておきます。
vminvhvwの短い方を利用してくれるものです。
その他細かいプロパティについてはMDN の gridをご覧下さい
const Grid = styled.div`
  display: grid;
  background: var(--bg);
  grid-template-rows: repeat(10, 10vmin);
  grid-template-columns: repeat(10, 10vmin);
`
それと位置指定を利用するので、このコンテナ用のコンポーネントを作っておきます
const Pos = styled.div`
  grid-row: ${({ row }) => row};
  grid-column: ${({ col }) => col};
  width: 100%;
  height: 100%;
`
これでこんな具合の Grid が出来ている事になります。ここに色々乗せていきます
stage0

木を作る

まず木です。木は長方形で十分なので、普通に div で作ります。
const Wood = styled(Pos)`
  background: #9e6946;
`

const App = () => (
  <Grid>
    <Wood row={"4/span 7"} col={"3/span 2"} />
  </Grid>
)
stage2

葉っぱを作る。

次に葉っぱを作りましょう。
クリスマスツリーの葉っぱなので三角形で作れば良さそうです。
CSS で三角形を作るとなるとborderが上下左右でナナメに接合していることを利用するハックが有名ですが、この場合 px 指定しか出来ないので grid で利用しようとすると厄介です。
そこで今回はclip-pathを利用します。
const Leafs = styled(Pos)`
  background: #1a9c5b;
  clip-path: polygon(0% 100%, 100% 100%, 50% 0%);
`
stage3-1
clip-path の良いところは点の位置を px でなく%で指定できるところです。
Grid のサイズに対して相対的に指定が出来て良いですね。(SVG のように path で指定する方法などもあります)
そして後は並べます
const App = () => (
  <Grid>
    <Wood row={"4/span 7"} col={"3/span 2"} />
    <Leafs row={"5/span 4"} col={"1/span 6"} />
    <Leafs row={"4/span 4"} col={"1/span 6"} />
    <Leafs row={"3/span 4"} col={"1/span 6"} />
    <Leafs row={"2/span 4"} col={"1/span 6"} />
  </Grid>
)
stage3-2
だいぶクリスマスツリーっぽくなってきましたね。

星を作る

これも clip-path で作っていきます。
ちょっと手打ちはきついので JS 側でそれっぽく計算させてます。若干雑です。
const calcStarPos = (p, len) =>
  [Math.cos(p) * len + 50, Math.sin(p) * len + 50]
    .map((r) => `${Math.ceil(r * 1000) / 1000}%`)
    .join(" ")

const outerPos = (num, i, len) =>
  calcStarPos(((360 / num) * i + 270) * (Math.PI / 180), len)

const innerPos = (num, i, len) =>
  calcStarPos(((360 / num) * (i + 3) + 90) * (Math.PI / 180), len)

const starPos = ({ num = 5, inner = 25, outer = 50 }) =>
  Array(num)
    .fill(null)
    .map((_, i) => [outerPos(num, i, outer), innerPos(num, i, inner)])
    .flat()
    .join(",")

export const Star = styled(Pos)`
  background: #f0ca4d;
  clip-path: polygon(${({ inner, outer }) => starPos({ inner, outer })});
`
お星さまができました。
stage4-1
乗せます
const App = () => (
  <Grid>
    <Wood row={"4/span 7"} col={"3/span 2"} />
    <Leafs row={"5/span 4"} col={"1/span 6"} />
    <Leafs row={"4/span 4"} col={"1/span 6"} />
    <Leafs row={"3/span 4"} col={"1/span 6"} />
    <Leafs row={"2/span 4"} col={"1/span 6"} />
    <Star row={"1/span 2"} col={"3/span 2"} outer={40} inner={20} />
  </Grid>
)
stage4-2

飾りを作る。

もうちょっと飾り付けをしましょう。
クリスマスツリーによくついてあるボンボン的な飾りも作ってみます。
単に丸ければいいのでborder-radiusで作っちゃいます。
export const Ball = styled(Pos)`
  background: ${({ color }) => color};
  border-radius: 100%;
`

const App = () => (
  <Grid>
    <Wood row={"4/span 7"} col={"3/span 2"} />
    <Leafs row={"5/span 4"} col={"1/span 6"} />
    <Leafs row={"4/span 4"} col={"1/span 6"} />
    <Leafs row={"3/span 4"} col={"1/span 6"} />
    <Leafs row={"2/span 4"} col={"1/span 6"} />
    <Star row={"1/span 2"} col={"3/span 2"} outer={40} inner={20} />
    <Ball row={"4"} col={"5"} color="purple" />
  </Grid>
)
さて、ここで表示してみるとこうなります
stage5-1
飾りが後ろに隠れてしまいました。
これが前述していた 「基本的には下が優先になる」に対する例外 になります。

対策:isolation を設定する

通常は下が優先になる重なりのルールですが、様々なケースで例外が発生します。
stacking context(と呼ばれるらしいです)のページを見ると、色々な例外が書いてあります。
この例外のうち、今回は三角の葉っぱに利用した clip-path が影響してしまってます。
い解決方法はありますが、ここではこれを解消するために、Positionisolation: isolateを仕掛けることで回避します
export const Pos = styled.div`
  grid-row: ${({ row }) => row};
  grid-column: ${({ col }) => col};
  width: 100%;
  height: 100%;
  isolation: isolate; // 追加
`
stage5-2
はい、今度はちゃんと出ました。
あとはこのまま数を増やせば飾りの完成です。
const App = () => (
  <Grid>
    {/* ... */}
    <Ball row={"4"} col={"5"} color="purple" />
    <Ball row={"5"} col={"3"} color="#4287f5" />
    <Ball row={"8"} col={"2"} color="#e38629" />
    <Ball row={"7"} col={"4"} color="#ebaec7" />
  </Grid>
)
stage5-3
ドットでGridを重ねてみるとこんな感じでマスにあわせて描画出来ているのがわかりますね
5-4

おまけ:色々着飾ってみる

さて、一旦ここまでで基礎としては出来ました。
ここからはあまりGrid関係ありませんが、いろんなプロパティを使ってゴチャつかせてみます。

飾りに立体感を出す

ちょっと立体感でも色気を出してつけたいので、radial-gradientを使いましょう。
export const Ball = styled(Pos)`
  background: ${({ color }) => color} radial-gradient(circle at 60% 35%, rgba(100%, 100%, 100%, 40%), transparent
        50%);
  border-radius: 100%;
`
 先程の background に重ねる形で radial-gradient を透過で設定する裏の色を使いながら gradient 出来ます
stage6-1

木に linear-gradient でストライプにする

木に repeat-linear-gradient でストライプ柄をつけてみます。
このテクニックについて詳しくは下記ブログが詳細に解説されています
Stripes in CSS
const Wood = styled(Pos)`
  background: #9e6946 repeating-linear-gradient(45deg, transparent 0%, transparent
        5%, #7a5136 5%, #7a5136 10%);
`
似たような感じで Leafs、Star にもかけてみるとこんな具合になります。
stage6-2

飾りを光らせてみる。

なんだかここまで来てものぺっとしてます。なので光らせてみましょう。
色々やり方はありますがまず飾りについては無難にbox-shadowでも使ってみましょう。
export const Ball = styled(Pos)`
  background: ${({ color }) => color} radial-gradient(circle at 60% 35%, rgba(100%, 100%, 100%, 40%), transparent
        50%);
  border-radius: 100%;
  box-shadow: 0px 0px 30px 2px yellow; /* 追加 */
`
星の方はclip-pathを使ってしまっているので、box-shadow を効かせることが出来ません。
なので、星と同じ位置にそれっぽいものを仕掛ける事で解決してみます。ここでも先程使ったradial-gradientを乗せてみます。
export const StarShadow = styled(Pos)`
  background: radial-gradient(rgba(100%, 100%, 0%, 80%), transparent 70%);
`

export const StarWithShadow = (props) => (
  <>
    <Star {...props} />
    <StarShadow {...props} />
  </>
)
stage8-1

雪を降らせる

最後に雪でも振らせてみたいと思います。
雪を降らせるやり方はたくさんあって手垢がついてる感じがありますが、今回はradial-gradientを使ったやり方でやってみましょう。
流石に CSS で乱数は難しいので、JS 側でradial-gradientを大量に並べるものを利用してみます。
const randomPositon = () => Math.random() * 90 + 5

const SnowLayer = styled.div.attrs(() => ({
  background: Array(200)
    .fill(null)
    .map(
      () =>
        `radial-gradient(circle at ${randomPositon()}% ${randomPositon()}%, white, transparent ${Math.random() *
          2}%)`
    )
    .join(",")
}))`
  background: ${({ background }) => background};
  height: 100%;
  width: 100%;
`
5%〜95%のどこかランダムに gradient を配置しまくるとこんな感じになります。
stage9-1
あとはこれを 3 枚ほど用意して-100%の位置から 100%の位置へ流れるようにズラしてアニメーションさせると雪が振ります。
delay などは見ながらいい感じに調整します
const animation = keyframes`
  0% {
    transform: translateY(-105%)
  }
  100% {
    transform: translateY(105%)
  }
`

const SnowAnimation = styled(Pos)`
  animation: ${animation} 10s infinite linear backwards;
  animation-delay: ${({ animationDelay }) => animationDelay}s;
`
const Snow = (props) => {
  return (
    <>
      {Array(3)
        .fill(null)
        .map((_, i) => (
          <SnowAnimation key={i} {...props} animationDelay={i * 4 - 10}>
            <SnowLayer />
          </SnowAnimation>
        ))}
    </>
  )
}
あとはこれを全体に上書きするように仕掛けてあげれば雪っぽくなります。
また、この snow はtranslateYを使っているで、はみ出た部分がそのままだと見えてしまいます。これを抑止したいのでGridoverflow: hiddenもつけます
export const Grid = styled.div`
  display: grid;
  background: black;
  padding: 1em;
  grid-template-rows: repeat(10, 5vmin);
  grid-template-columns: repeat(10, 5vmin);
  overflow: hidden; // 追加
`

const App = () => (
  <>
    <Grid>
      {/* ... */}
      <Ball row={"7"} col={"5"} color="lightpink" />
      <Snow row={"1 / 10"} col={"1 / 9"} /> {/* 追加 */}
    </Grid>
  </>
)
demo

最終形

こんな感じになりました。これで良い年末が過ごせますね。
https:stackblitz.com/edit/react-ts-yqye76
Edit on Github