はじめに
ReactとStyled-compomentsを使って、シンプルに12カラムのグリッドレイアウトを実装してみました。
カラムレイアウトにはFlexboxを使っています。
Reactの環境構築はcreate-react-appで行います。
なお、Styled-compomentsの概要については省略します。
グリッドレイアウトの概要
- 12カラムのグリッドに沿って、横並びやレスポンシブのアイテムを配置していきます。
- 各カラムの左右にはガター(溝)のpaddingがあります。12カラム全体では左右に(ガター/2)分のpaddingとなります。
グリッドレイアウトについてあまり聞きなれない場合は下記リンク等が参考になると思います。
Bootstrapのグリッドシステムの使い方を初心者に向けておさらいする
Grid system - Bootstrap 4.5 - 日本語リファレンス
1. Reactプロジェクトの準備
CRAをTypeScript付きでプロジェクト作成し、Styled-compoments関連のパッケージもインストールします。
$ npx create-react-app grid-layout-styled-components --typescript$ cd grid-layout-styled-components
$ npm install--save styled-components
$ npm install @types/styled-components
$ npm install--save-dev babel-plugin-styled-components
次にbabel-plugin-styled-componentsを使うための準備をします。
$ touch .babelrc
.babelrc
{"plugins":[["babel-plugin-styled-components"]]}
最後にindex.cssとApp.cssの中身を一旦空にしておきます。
2. グローバルスタイルの作成
グローバルに適用するスタイルを作成します。
$ touch src/GlobalStyle.ts
はじめにリセットCSSを追加します。好みによりNormalizeCSSでも可能だと思います。
リセットCSSの中では個人的にEric Meyer氏のものがシンプルで好きです。
import{createGlobalStyle}from'styled-components/macro';exportconstGlobalStyle=createGlobalStyle`
/* Reset CSS */
/* ===================================== */
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
}
`;
次にタイプセレクタへのCSSを追加します。
ここで必須なのは*, *::before, *::after{box-sizing: border-box;}
になります。
ついでに必要ないとは思いますが、フォントはいつでも綺麗にしておきたい性分なのでbody
へのfont-family
設定も癖で追加しました。
import{createGlobalStyle}from'styled-components/macro';exportconstGlobalStyle=createGlobalStyle`
/* Reset CSS */
/* ===================================== */
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* Add Global CSS */
/* ===================================== */
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
`;
3. 定数の作成
App.tsxに書いていきます。以下を用意します。
- ブレークポイント
- 各ブレークポイントに対応するコンテナの最大幅
- グリッドの溝
- カラム数
importReactfrom'react';importstyledfrom'styled-components/macro';import{GlobalStyle}from'./GlobalStyle';import'./App.css';// configsconstbreakpoints:{sm:number;md:number;lg:number;xl:number}={sm:576,md:768,lg:992,xl:1280,};constcontainerMaxWidths:{sm:number;md:number;lg:number;xl:number}={sm:540,md:720,lg:960,xl:1250,};constgridColumns:number=12;constgridGutterWidth:number=32;// componentsfunctionApp(){return<div><GlobalStyle/></div>;
}exportdefaultApp;
4. 汎用コンポーネント作成
汎用的に使うコンテナのコンポーネントと、グリッド行・グリッド列のコンポーネントを用意します。
実装にあたりBootstrapのscssやcssのソースを参考にしました。
全て理解するのは難解ですが、一つ一つは(Bootstrapを使うかに関係なく)CSSの設計に勉強になることが多いと感じます。
importReactfrom'react';importstyledfrom'styled-components/macro';import{GlobalStyle}from'./GlobalStyle';import'./App.css';// configsconstbreakpoints:{sm:number;md:number;lg:number;xl:number}={sm:576,md:768,lg:992,xl:1280,};constcontainerMaxWidths:{sm:number;md:number;lg:number;xl:number}={sm:540,md:720,lg:960,xl:1250,};constgridColumns:number=12;constgridGutterWidth:number=32;// componentsconstContainer=styled.div`
max-width: 100%;
@media (min-width: ${breakpoints.sm}px) {
max-width: ${containerMaxWidths.sm}px;
}
@media (min-width: ${breakpoints.md}px) {
max-width: ${containerMaxWidths.md}px;
}
@media (min-width: ${breakpoints.lg}px) {
max-width: ${containerMaxWidths.lg}px;
}
@media (min-width: ${breakpoints.xl}px) {
max-width: ${containerMaxWidths.xl}px;
}
padding-right: ${gridGutterWidth/2}px;
padding-left: ${gridGutterWidth/2}px;
margin-right: auto;
margin-left: auto;
`;constRow=styled.div`
display: flex;
flex-wrap: wrap;
margin-right: ${-gridGutterWidth/2}px;
margin-left: ${-gridGutterWidth/2}px;
`;typeColProps={sizeDefault:number;sizeSm?:number;sizeMd?:number;sizeLg?:number;sizeXl?:number;};constCol=styled.div<ColProps>`
flex: 0 0 ${(props)=>(props.sizeDefault/gridColumns)*100}%;
max-width: ${(props)=>(props.sizeDefault/gridColumns)*100}%;
@media (min-width: ${breakpoints.sm}px) {
flex: 0 0 ${(props)=>((props.sizeSm||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeSm||props.sizeDefault)/gridColumns)*100}%;
}
@media (min-width: ${breakpoints.md}px) {
flex: 0 0 ${(props)=>((props.sizeMd||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeMd||props.sizeDefault)/gridColumns)*100}%;
}
@media (min-width: ${breakpoints.lg}px) {
flex: 0 0 ${(props)=>((props.sizeLg||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeLg||props.sizeDefault)/gridColumns)*100}%;
}
@media (min-width: ${breakpoints.xl}px) {
flex: 0 0 ${(props)=>((props.sizeXl||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeXl||props.sizeDefault)/gridColumns)*100}%;
}
padding-right: ${gridGutterWidth/2}px;
padding-left: ${gridGutterWidth/2}px;
`;functionApp(){return<div><GlobalStyle/></div>;
}exportdefaultApp;
ここで各コンポーネントの解説をします。
なお前提として、モバイルファーストで作っています。
Container
コンポーネント
幅はブレークポイントに対応する最大幅を設定します。
中央寄せした上で、グリッド溝/2を左右のpadding
に与えます。
Row
コンポーネント
はじめにdisplay
をflex
にセットして、
グリッド溝/2のネガティブマージンを左右のmargin
に与えます。
Col
コンポーネント
幅は12グリッドのいくつ分を占めるかをpropsとして受け取れるようにします。
デフォルトの幅は必須で、sm/md/lg/xl用の幅は任意とします。
※sizeDefaultの名前は最初sizeとする予定だったのですが、元々HTMLにsizeという属性があるようなので止めました。
flexの他にmax-widthにおいても幅を設定していますが、前者だけだと特定ブラウザで動かないようなので後者も設定する必要があるらしいです。(Bootstrapのソースコメントによると)
この辺りは申し訳無いのですが調べずにスルーしています。
余談
なるべくシンプルにという方針で、多少1つのブロックにつき繰り返しは多くなりそうですがcss prop
を使わない(Sassでいうmixin
のような用法)方針で実装しました。
5. 実際に使ってみる
前章のコンポーネントを使ってみます。
グリッドを適用する場合はRowを記述し、その子にColをサイズのpropsと共に指定します。
.side-border{border-right:1pxsolid#000;border-left:1pxsolid#000;}
importReactfrom'react';importstyledfrom'styled-components/macro';import{GlobalStyle}from'./GlobalStyle';import'./App.css';// configsconstbreakpoints:{sm:number;md:number;lg:number;xl:number}={sm:576,md:768,lg:992,xl:1280,};constcontainerMaxWidths:{sm:number;md:number;lg:number;xl:number}={sm:540,md:720,lg:960,xl:1250,};constgridColumns:number=12;constgridGutterWidth:number=32;// componentsconstContainer=styled.div`
max-width: 100%;
@media (min-width: ${breakpoints.sm}px) {
max-width: ${containerMaxWidths.sm}px;
}
@media (min-width: ${breakpoints.md}px) {
max-width: ${containerMaxWidths.md}px;
}
@media (min-width: ${breakpoints.lg}px) {
max-width: ${containerMaxWidths.lg}px;
}
@media (min-width: ${breakpoints.xl}px) {
max-width: ${containerMaxWidths.xl}px;
}
padding-right: ${gridGutterWidth/2}px;
padding-left: ${gridGutterWidth/2}px;
margin-right: auto;
margin-left: auto;
`;constRow=styled.div`
display: flex;
flex-wrap: wrap;
margin-right: ${-gridGutterWidth/2}px;
margin-left: ${-gridGutterWidth/2}px;
`;typeColProps={sizeDefault:number;sizeSm?:number;sizeMd?:number;sizeLg?:number;sizeXl?:number;};constCol=styled.div<ColProps>`
flex: 0 0 ${(props)=>(props.sizeDefault/gridColumns)*100}%;
max-width: ${(props)=>(props.sizeDefault/gridColumns)*100}%;
@media (min-width: ${breakpoints.sm}px) {
flex: 0 0 ${(props)=>((props.sizeSm||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeSm||props.sizeDefault)/gridColumns)*100}%;
}
@media (min-width: ${breakpoints.md}px) {
flex: 0 0 ${(props)=>((props.sizeMd||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeMd||props.sizeDefault)/gridColumns)*100}%;
}
@media (min-width: ${breakpoints.lg}px) {
flex: 0 0 ${(props)=>((props.sizeLg||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeLg||props.sizeDefault)/gridColumns)*100}%;
}
@media (min-width: ${breakpoints.xl}px) {
flex: 0 0 ${(props)=>((props.sizeXl||props.sizeDefault)/gridColumns)*100}%;
max-width: ${(props)=>((props.sizeXl||props.sizeDefault)/gridColumns)*100}%;
}
padding-right: ${gridGutterWidth/2}px;
padding-left: ${gridGutterWidth/2}px;
`;// UsageconstHeading=styled.h1`
font-size: 32px;
font-weight: bold;
padding: 24px 0;
`;typeInnerContentProps={height:number;backgroundColor:string};constInnerContent=styled.div<InnerContentProps>`
height: ${(props)=>props.height}px;
background-color: ${(props)=>props.backgroundColor};
`;functionApp(){return(<div><GlobalStyle/><ContainerclassName="side-border"><Heading>12Gridsystem</Heading>
<Row>{[0,1,2,3,4,5,6,7,8,9,10,11].map((v,index)=>(<Colkey={index}sizeDefault={1}><InnerContentheight={600}backgroundColor="deepskyblue"></InnerContent>
</Col>
))}</Row>
<Heading>Responsive1</Heading>
<Row><ColsizeDefault={12}sizeMd={4}sizeLg={4}sizeXl={4}><InnerContentheight={200}backgroundColor="lightgray"></InnerContent>
</Col>
<ColsizeDefault={12}sizeMd={4}sizeLg={4}sizeXl={4}><InnerContentheight={200}backgroundColor="darkgray"></InnerContent>
</Col>
<ColsizeDefault={12}sizeMd={4}sizeLg={4}sizeXl={4}><InnerContentheight={200}backgroundColor="gray"></InnerContent>
</Col>
</Row>
<Heading>Responsive2</Heading>
<Row><ColsizeDefault={12}sizeMd={12}sizeLg={4}sizeXl={4}><InnerContentheight={300}backgroundColor="gold"></InnerContent>
</Col>
<ColsizeDefault={12}sizeMd={12}sizeLg={8}sizeXl={8}><InnerContentheight={300}backgroundColor="goldenrod"></InnerContent>
</Col>
</Row>
<Heading>Responsive3</Heading>
<Row><ColsizeDefault={12}sizeLg={8}sizeXl={8}><Row><ColsizeDefault={6}><InnerContentheight={150}backgroundColor="blue"></InnerContent>
</Col>
<ColsizeDefault={6}><InnerContentheight={150}backgroundColor="darkblue"></InnerContent>
</Col>
<ColsizeDefault={6}><InnerContentheight={150}backgroundColor="dodgerblue"></InnerContent>
</Col>
<ColsizeDefault={6}><InnerContentheight={150}backgroundColor="royalblue"></InnerContent>
</Col>
</Row>
</Col>
<ColsizeDefault={12}sizeLg={4}sizeXl={4}><InnerContentheight={300}backgroundColor="crimson"></InnerContent>
</Col>
</Row>
</Container>
</div>
);}exportdefaultApp;
上記を出力するとこのようになります。
実際の動きは下記GitHubからリンクを参照してください。