こんにちは!Life is Tech! でunityメンターをしております、たおと申します!
この記事はLife is Tech ! Advent Calendar 2023 4日目の記事です。
はじめに
みなさんはweb制作の際のcss設計について考えたことはありますか?
cssは記法が自由であり、タグやクラス名から好きなように設計することができる魅力的なツールです。
しかし、その自由さゆえにcssが重複してしまったり、!important
を多用してしまったりと破綻に向かっていくケースも多くあります。
ここで登場するのがFLOCSS(フロックス)であり、その保守性の高さから注目を集めている記法です。
今回はFLOCSSについて、実際にNext.jsを使用した例も交えて紹介していけたらと思います!
目次
はじめに
そもそもFLOCSSとは
FLOCSSのメリット
プロジェクト設計
そもそもFLOCSSとは?
Foundation Layout Object CSSの略です。
├─ Foundation
├─ Layout
└─ Object
├─ Component
├─ Project
└─ Utility
cssを役割ごとに分割することで、崩れにくい構成を実現しています。
各要素の説明(公式ドキュメントより引用)
- ・Foundation
- プロジェクトにおける基本的なスタイル
- ・Layout
- プロジェクト共通のコンテナーブロックのスタイル
- ・Object
- プロジェクトにおける基本的なスタイルを定義
- 1. Component
- 再利用可能な小さな単位のモジュール
- 2. Project
- プロジェクト固有のパターンであり、いくつかのComponentと、それに該当しない要素によって構成されるもの
- 3. Utility
- わずかなスタイルの調整のための便利クラスなど
class命名法
BEM記法をベースにします。
Blocks-Elements-Modifiersの略で、
- ・Block
- 再利用可能な枠組み
- ・Element
- それだけでは独立できないBlock内の要素
- ・Modifier
- 色や大きさなどのスタイル
のような意味づけがされています。
.Block__Element--modifier
の形で記述し、複数語になる場合はケバブケース(区切り文字としてハイフン-
を使用)をとります。
(例:class="p-change-password__input p-change-password__input--hidden"
)
ただし、modifierを多く使用するとクラス名が冗長となり、かえってわかりにくくなるので私はmodifierのみのクラスを用いています。
(例:class="p-change-password__input -hidden"
)
FLOCSSでは、主にBlockの部分の記法を与えます。
特にLayoutにはl-, Componentにはc-, Projectにはp-, Utilityにはu-を先頭につけてそれぞれのクラスの役割を明示します。
FLOCSSにおいてはidやタグは用いず、全てのスタイル対象にクラス名を割り振ります。
FLOCSSのメリット
細かなルールが設けられているため、複数人で同じプロジェクトに取り組んでも破綻しにくいです。
CSSそのものをコンポーネントごとに分割しているので、再利用しやすいです。
命名がその機能に即しているので、class名を見た際にそれが何をするブロックなのかが一目でわかります。
実際のプロジェクト設計
Next.js(Typescript) での例を考えます。
(もちろん他のフレームワークでも有効です!)
環境構築
まだの方はnode.jsをダウンロードしてください。
Next.jsのプロジェクト作成
npx create-next-app my-app
すると以下のように選択肢が提示されますので、お好みで設定してください。
そして
cd my-app
npm run dev
とすると http://localhost:3000 にアクセスして以下のように表示されていればOKです。
絶対パスの導入
const path = require('path');
const nextConfig = {
reactStrictMode: true,
webpack(config, options) {
config.resolve.alias['@'] = path.join(__dirname, 'src')
return config;
},
}
module.exports = nextConfig
これで、import
などの際に相対パスで記述する手間が省けます。
例:@/styles/style.scss
sassの導入
npm i -D sass
ディレクトリ構成
私が使用しているものになりますが、ディレクトリ構成の一例を以下に示します。
src
├─ app
│ ├─ api/
│ ├─ page1
│ │ ├─ layout.tsx
│ │ └─ page.tsx
│ ├─ page2/
│ ├─ ...
│ ├─ not-found.tsx
│ ├─ layout.tsx
│ └─ page.tsx
├─ components
│ ├─ button.tsx
│ └─ ...
├─ assets
│ ├─ img/
│ └─ favicon.ico
├─ const/
├─ models/
├─ lib/
├─ styles
│ ├─ foundation
│ │ ├─ _base.scss
│ │ ├─ _mixin.scss
│ │ ├─ _variables.scss
│ │ └─ _index.scss
│ ├─ layout
│ │ ├─ _main.scss
│ │ └─ _nav.scss
│ ├─ component
│ │ ├─ _button.scss
│ │ └─ ...
│ ├─ project
│ │ ├─ page1
│ │ │ ├─ ...
│ │ │ └─ _index.scss
│ │ ├─ page2/
│ │ └─ ...
│ ├─ utility
│ │ ├─ align
│ │ ├─ ...
│ │ └─ margin
│ └─ style.scss
└─ types/
ディレクトリの解説
app
Next.js 13のリリースで、app/
ディレクトリが実装されました。
以下のようにnext.confignjs
に追記することで使用可能です。
const nextConfig = {
...
experimental: {
appDir: true,
},
}
本題ではないので詳細は割愛しますが、各ディレクトリにpage.tsx
とlayout.tsx
などを配置すると、そのままディレクトリ名をルートとして扱い、また共通のレイアウトの定義も簡単に実装することができます。
components
プロジェクト全体で再利用できる最小のReactコンポーネントを格納します。
例:button.tsx
, filter.tsx
, pagination.tsx
後述しますがFLOCSSのComponentと同じ単位で扱います。
assets
画像やfaviconなどの静的な素材を格納します。
const
文字データなど、定数を定義するファイルを格納します。
例:const.tsx
export const API_BASE_URL = 'https://your_base_url'
models
各ページなどで使用するデータモデルを定義します。
例:userDataModel.tsx
lib
ライブラリなどを格納します。
例:mysql.ts
types
型定義ファイルを格納します。
例:sample.d.ts
styles
今回の本題であり、スタイルシートを格納するディレクトリです。
記法がDartSassに切り替わったため、@import
は廃止され@use
と@forward
を使用します。
各ディレクトリに_index.scssを配置し、そこに@forward
でディレクトリ内のscssを読み込ませ、style/
直下のstyle.scss
で全てを読み込む方式を取ります。
- 構成
object/
ディレクトリは使用せず、component/
、project/
、utility/
をそのままstyles/
直下に作成します。
styles
├─ foundation
├─ ...
├─ project/setting
│ ├─ _setitng.scss
│ ├─ _setting-menu.scss
│ ├─ ...
│ └─ _index.scss
└─ style.scss
- index.scssで読み込み
@forward "setting";
@forward "setting-menu";
//...
- style.scssでまとめて読み込み
//foundation
@use "foundation/base";
@use "foundation/variable";
@use "foundation/mixin";
//layout
@use "layout/main";
@use "layout/nav";
//object
//component
@use "component/button";
@use "component/filter";
//...
//project
@use "project/signin";
@use "project/setting";
@use "project/notfound";
//...
//utility
@use "utility/margin";
@use "utility/align";
//...
FLOCSSディレクトリの解説
プロジェクト全体で共通のスタイルを定義します。
variablesやmixinなどもここで定義します。
resetCSSもここで定義する場合もありますが、私はCDNで読み込んでいます。(メンテナンスに柔軟に対応できるため)
import '@/styles/style.scss'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/destyle.css@1.0.15/destyle.css" />
</head>
<body>{children}</body>
</html>
)
}
variables.scss
色やフォントサイズなどを定義します。
//colors
$white: #ffffff;
$black: #1e1e1e;
$gray: #bababa;
$dark-gray: #636363;
$pale-gray: #e1e1e1;
$light-gray: #f7f7f7;
$blue: #004df2;
$light-blue: #437fff;
$pale-blue: #eaf1ff;
$yellow: #f2f200;
$red: #ff437f;
$green: #7fff43;
//font size
$fs-title: 2.0rem;
$fs-sub_title: 1.8rem;
$fs-text: 1.4rem;
$fs-input: 1.6rem;
$fs-medium: 1.6rem;
$fs-small: 1.2rem;
$fs-smaller: 1.0rem;
$fs-x-small: 0.8rem;
// break points
$breakpoints: (
pc: 1280px,
tb: 960px,
sp: 560px,
min: 360px,
) !default;
mixin.scss
mixinを定義します。特にメディアクエリの定義にも使用したりします。
@use 'variable' as *;
@mixin contentBox($width: 200px, $height: 200px, $color: var.$white, $scrollable: false) {
background-color: $color;
width: $width;
height: $height;
border-radius: 10px;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
@if $scrollable {
overflow-y: scroll;
}
}
@mixin mq($breakpoint: md) {
@media screen and (max-width: #{map-get($breakpoints, $breakpoint)}) {
@content;
}
}
base.scss
全体のスタイルを定義します。
@use 'variable' as *;
html {
font-size: 62.5%;
}
body {
background-color: $white;
color: $black;
font-family: Avenir, Helvetica;
display: flex;
place-items: center;
box-sizing: border-box;
text-align: center;
cursor: default;
}
a {
color: $blue;
text-decoration: none;
cursor: pointer;
}
ul, ol, li {
list-style: none;
}
コンテンツエリアはもちろん、ヘッダー、サイドバーなどのプロジェクト全体で使用するレイアウトに係るスタイルを定義します。唯一idを使用してもよいスタイルシートです。
html側でも.l-***
とクラスを命名します。
main.scss
全体のレイアウトを定義します。
@use '@/styles/foundation' as var;
.l-main {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
}
.l-nav {
width: 250px;
position: sticky;
left: 0;
z-index: 2;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
}
.l-content {
position: relative;
width: calc(100vw - 250px);
}
.l-overlay {
z-index: 3;
position: absolute;
&.-hidden {
z-index: 3;
}
}
.l-animation {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
height: 200px;
width: 200px;
background: none;
z-index: 3;
}
その他、_header.scss
や_footer.scss
など適宜スタイリングします。
再利用可能な最小単位のコンポーネントのスタイリングです。
src/component
ディレクトリ内のReactコンポーネントと1対1対応します。
最小単位なのでもちろんネストしてはいけません。
html側では.c-***
とクラスを命名します。
button.tsx
import { MouseEventHandler } from "react"
interface Props {
addClass?: string,
onClick?: MouseEventHandler,
label?: string,
}
export const Button = (props: Props) => {
return (
<button
className={`c-button ${props.addClass}`}
onClick={props.onClick}
>
{props.label}
</button>
)
}
button.scss
@use '@/styles/foundation' as *;
.c-button {
height: 30px;
width: 150px;
border: 1px solid $gray;
color: $black;
background-color: $white;
border-radius: 5px;
box-shadow: none;
font-size: $fs-text;
text-align: center;
cursor: pointer;
&.-large{
height: 40px;
width: 200px;
}
}
このようにmodifierなどを用いてスタイルの種類を増やします。
そのコンポーネント以外でも同じように使用できるスタイル(marginやalignなど)を変更したい場合はutilityで調整します。
いくつかのコンポーネントの集合で、使い回すこともできる大きいブロックを指します。ページごとにディレクトリを作成し、そのページ固有のブロックを定義していきます。
html側では.p-***
とクラスを命名します。
構成
project
├─ page1
├─ ...
├─ setting
│ ├─ _setitng.scss
│ ├─ _setting-menu.scss
│ ├─ ...
│ └─ _index.scss
└─ style.scss
setting/page.tsx
'use client'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { BackButton } from 'src/components/backbutton'
export const Setting = () => {
const router = useRouter()
return (
<div className="p-setting">
<BackButton addClass="p-setting__back-button" />
<div className="p-setting__title">設定</div>
<div className="p-setting__main">
<div className="p-setting-menu">
<div
className="p-setting-menu__item"
onClick={() => {
router.push("account")
}}
>
<Image
className="p-setting-menu__item-img"
src="asset/img/account.png"
alt="アカウント"
/>
<div className="p-setting-menu__item-label">アカウント</div>
<div className="p-setting-menu__item-explanation">パスワードの変更などができます</div>
</div>
<div
className="p-setting-menu__item"
onClick={() => {
router.push("notification")
}}
>
<Image className="p-setting-menu__item-img" src="asset/img/notify.png" alt="通知" />
<div className="p-setting-menu__item-label">通知</div>
<div className="p-setting-menu__item-explanation">通知内容や頻度を変更できます</div>
</div>
</div>
</div>
</div>
)
}
設定画面内にp-setting
、p-setting-menu
、p-setting-content
の3つのprojectが存在しています。
それぞれについてproject/
内にscssを作成してスタイリングしていきます。
setting.scss
@use "@/styles/foundation" as *;
.p-setting{
@include contentBox(100%, 100%, $white);
position: relative;
padding: 30px 0px;
&__title{
font-size: $fs-title;
font-weight: bold;
text-align: left;
padding-left: 30px;
}
&__main{
padding: 0px 30px;
}
&__back-button{
position: absolute;
top: 30px;
left:30px;
}
}
他のprojectについても同様にスタイリングしていきます。
簡単ではありますがこんな感じの見た目になりました。
componentに書いてはいけない、またprojectでも書くべきでないようなmarginなどのスタイルについて!important
を使用して調整します。
html側では.u-***
とクラスを命名します。
margin.scss
各方向のmargin調整用です。
@use "@/styles/foundation" as *;
@each $space in $spaces {
.u-m#{ $space } {
margin: #{ $space }px !important;
}
.u-mb#{ $space } {
margin-bottom: #{ $space }px !important;
}
//...
}
color.scss
色の調整用です。
@use "@/styles/foundation" as *;
.u-bg-blue {
background-color: $blue !important;
}
.u-blue {
color: $blue !important;
}
//...
使用例
<Button addClass="p-signin__submit-button u-mb16" />
ButtonはReactコンポーネントですが、button.scssにはmarginなどについて書いてはいけません。そのためu-mb16
(margin-bottom: 16px
)を与えることでスタイルを与えられます。
まとめ
長くなってしまいましたが、FLOCSSについての理解は深まりましたでしょうか?
チーム開発などにおいて、コードの保守性は開発の効率を上げる重要なポイントになります。
ルールが難しかったり命名が大変だったりと慣れるのに時間はかかるかもしれませんが、class名からその機能の推測も簡単になりますし、拡張性も非常に高いので導入するメリットは大いにあると思います。
今後のweb開発において、綺麗で引き継ぎやすいプロジェクトづくりを目指す際に参考にしていただけると幸いです!