posted: 2019/12/04
styled-componentsで作る年末年始にぴったりなCSS Gridにおける要素の重なりの話
この記事は CSS Advent Calendar 2019 4 日目の記事です。
今回は CSS Grid の要素の重なりについて記述します。
なお、styled-components を利用している点についてはご容赦いただければと思います。
なお、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>
)
はい、赤い要素(Item2)が上に来ました
では逆にすると
const App = () => (
<Grid>
<Item1 col="1" row="1/3">
Item1
</Item1>
<Item2 col="1/3" row="1">
Item2
</Item2>
</Grid>
)
青い要素(Item1)が上に来ました。
このように基本的に下に来る要素が上に来る形になります(例外はありますがこれは後述)
このように基本的に下に来る要素が上に来る形になります(例外はありますがこれは後述)
この特性を使えば独自定義の座標に基づいて要素を並べて図画を描画出来ます。
すべての対象要素に
grid-row
とgrid-column
を付けなければ行けないという欠点がありますが、「px 指定でなく%
指定でレスポンシブなものを作れる」「SVG でやりづらかったことを CSS 側で処理できる(割と無いかも?)」などの利点があります。年末なのでクリスマスツリーを作る
さて、これを使えば CSS でお絵かきが出来ます。
今回はクリスマスツリーを作ってデコレーションしてみましょう。
今回はクリスマスツリーを作ってデコレーションしてみましょう。
また、今回せっかく Grid を活かすために、
position: absolute
やpx
指定はなるべく縛って行こうと思います。Grid 基盤
その他細かいプロパティについては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 が出来ている事になります。ここに色々乗せていきます
木を作る
まず木です。木は長方形で十分なので、普通に div で作ります。
const Wood = styled(Pos)`
background: #9e6946;
`
const App = () => (
<Grid>
<Wood row={"4/span 7"} col={"3/span 2"} />
</Grid>
)
葉っぱを作る。
次に葉っぱを作りましょう。
クリスマスツリーの葉っぱなので三角形で作れば良さそうです。
CSS で三角形を作るとなると
そこで今回はclip-pathを利用します。
クリスマスツリーの葉っぱなので三角形で作れば良さそうです。
CSS で三角形を作るとなると
border
が上下左右でナナメに接合していることを利用するハックが有名ですが、この場合 px 指定しか出来ないので grid で利用しようとすると厄介です。そこで今回はclip-pathを利用します。
const Leafs = styled(Pos)`
background: #1a9c5b;
clip-path: polygon(0% 100%, 100% 100%, 50% 0%);
`
clip-path の良いところは点の位置を px でなく
Grid のサイズに対して相対的に指定が出来て良いですね。(SVG のように path で指定する方法などもあります)
%
で指定できるところです。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>
)
だいぶクリスマスツリーっぽくなってきましたね。
星を作る
これも clip-path で作っていきます。
ちょっと手打ちはきついので JS 側でそれっぽく計算させてます。若干雑です。
ちょっと手打ちはきついので 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 })});
`
お星さまができました。
乗せます
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>
)
飾りを作る。
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>
)
さて、ここで表示してみるとこうなります
飾りが後ろに隠れてしまいました。
これが前述していた 「基本的には下が優先になる」に対する例外 になります。
これが前述していた 「基本的には下が優先になる」に対する例外 になります。
対策:isolation を設定する
通常は下が優先になる重なりのルールですが、様々なケースで例外が発生します。
stacking context(と呼ばれるらしいです)のページを見ると、色々な例外が書いてあります。
この例外のうち、今回は三角の葉っぱに利用した
clip-path
が影響してしまってます。い解決方法はありますが、ここではこれを解消するために、
Position
にisolation: isolate
を仕掛けることで回避しますexport const Pos = styled.div`
grid-row: ${({ row }) => row};
grid-column: ${({ col }) => col};
width: 100%;
height: 100%;
isolation: isolate; // 追加
`
はい、今度はちゃんと出ました。
あとはこのまま数を増やせば飾りの完成です。
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>
)
ドットでGridを重ねてみるとこんな感じでマスにあわせて描画出来ているのがわかりますね
おまけ:色々着飾ってみる
さて、一旦ここまでで基礎としては出来ました。
ここからはあまりGrid関係ありませんが、いろんなプロパティを使ってゴチャつかせてみます。
ここからはあまり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 出来ます
木に linear-gradient でストライプにする
木に repeat-linear-gradient でストライプ柄をつけてみます。
このテクニックについて詳しくは下記ブログが詳細に解説されています
Stripes in CSS
Stripes in CSS
const Wood = styled(Pos)`
background: #9e6946 repeating-linear-gradient(45deg, transparent 0%, transparent
5%, #7a5136 5%, #7a5136 10%);
`
似たような感じで Leafs、Star にもかけてみるとこんな具合になります。
飾りを光らせてみる。
なんだかここまで来てものぺっとしてます。なので光らせてみましょう。
色々やり方はありますがまず飾りについては無難に
色々やり方はありますがまず飾りについては無難に
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} />
</>
)
雪を降らせる
最後に雪でも振らせてみたいと思います。
雪を降らせるやり方はたくさんあって手垢がついてる感じがありますが、今回は
流石に CSS で乱数は難しいので、JS 側で
雪を降らせるやり方はたくさんあって手垢がついてる感じがありますが、今回は
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 を配置しまくるとこんな感じになります。
あとはこれを 3 枚ほど用意して-100%の位置から 100%の位置へ流れるようにズラしてアニメーションさせると雪が振ります。
delay などは見ながらいい感じに調整します
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 は
また、この snow は
translateY
を使っているで、はみ出た部分がそのままだと見えてしまいます。これを抑止したいのでGrid
にoverflow: 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>
</>
)
最終形
こんな感じになりました。これで良い年末が過ごせますね。