LoginSignup
3
1

0から始めるFLOCSS

Last updated at Posted at 2023-12-03

こんにちは!Life is Tech! でunityメンターをしております、たおと申します!
この記事はLife is Tech ! Advent Calendar 2023 4日目の記事です。

はじめに

みなさんはweb制作の際のcss設計について考えたことはありますか?
cssは記法が自由であり、タグやクラス名から好きなように設計することができる魅力的なツールです。
しかし、その自由さゆえにcssが重複してしまったり、!importantを多用してしまったりと破綻に向かっていくケースも多くあります。

ここで登場するのがFLOCSS(フロックス)であり、その保守性の高さから注目を集めている記法です。
今回はFLOCSSについて、実際にNext.jsを使用した例も交えて紹介していけたらと思います!

目次

はじめに
そもそもFLOCSSとは
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です。

絶対パスの導入

next.config.js
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に追記することで使用可能です。

next.config.js
const nextConfig = {
    ...
    experimental: {
        appDir: true,
    },
}

本題ではないので詳細は割愛しますが、各ディレクトリにpage.tsxlayout.tsxなどを配置すると、そのままディレクトリ名をルートとして扱い、また共通のレイアウトの定義も簡単に実装することができます。

components

プロジェクト全体で再利用できる最小のReactコンポーネントを格納します。
例:button.tsx, filter.tsx, pagination.tsx

後述しますがFLOCSSのComponentと同じ単位で扱います。

assets

画像やfaviconなどの静的な素材を格納します。

const

文字データなど、定数を定義するファイルを格納します。

例:const.tsx

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で読み込み
setting/_index.scss
@forward "setting";
@forward "setting-menu";
//...
  • style.scssでまとめて読み込み
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ディレクトリの解説

  • foundation/

プロジェクト全体で共通のスタイルを定義します。
variablesやmixinなどもここで定義します。

resetCSSもここで定義する場合もありますが、私はCDNで読み込んでいます。(メンテナンスに柔軟に対応できるため)

app/layout.tsx
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
色やフォントサイズなどを定義します。

_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を定義します。特にメディアクエリの定義にも使用したりします。

_mixin.scss
@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
全体のスタイルを定義します。

_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;
}
  • layout/

コンテンツエリアはもちろん、ヘッダー、サイドバーなどのプロジェクト全体で使用するレイアウトに係るスタイルを定義します。唯一idを使用してもよいスタイルシートです。

html側でも.l-***とクラスを命名します。

main.scss
全体のレイアウトを定義します。

_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など適宜スタイリングします。

  • component/

再利用可能な最小単位のコンポーネントのスタイリングです。
src/componentディレクトリ内のReactコンポーネントと1対1対応します。
最小単位なのでもちろんネストしてはいけません。

html側では.c-***とクラスを命名します。

button.tsx

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

_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で調整します。

  • project/

いくつかのコンポーネントの集合で、使い回すこともできる大きいブロックを指します。ページごとにディレクトリを作成し、そのページ固有のブロックを定義していきます。

html側では.p-***とクラスを命名します。

構成

project
├─ page1
├─ ...
├─ setting
│  ├─ _setitng.scss
│  ├─ _setting-menu.scss
│  ├─ ...
│  └─ _index.scss
└─ style.scss

setting/page.tsx

setting.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-settingp-setting-menup-setting-contentの3つのprojectが存在しています。
それぞれについてproject/内にscssを作成してスタイリングしていきます。

setting.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についても同様にスタイリングしていきます。

簡単ではありますがこんな感じの見た目になりました。

  • utility/

componentに書いてはいけない、またprojectでも書くべきでないようなmarginなどのスタイルについて!importantを使用して調整します。

html側では.u-***とクラスを命名します。

margin.scss
各方向のmargin調整用です。

_margin.scss
@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
色の調整用です。

_color.scss
@use "@/styles/foundation" as *;

.u-bg-blue {
    background-color: $blue !important;
}

.u-blue {
    color: $blue !important;
}

//...

使用例

setting/page.tsx
<Button addClass="p-signin__submit-button u-mb16" />

ButtonはReactコンポーネントですが、button.scssにはmarginなどについて書いてはいけません。そのためu-mb16 (margin-bottom: 16px)を与えることでスタイルを与えられます。

まとめ

長くなってしまいましたが、FLOCSSについての理解は深まりましたでしょうか?
チーム開発などにおいて、コードの保守性は開発の効率を上げる重要なポイントになります。
ルールが難しかったり命名が大変だったりと慣れるのに時間はかかるかもしれませんが、class名からその機能の推測も簡単になりますし、拡張性も非常に高いので導入するメリットは大いにあると思います。


今後のweb開発において、綺麗で引き継ぎやすいプロジェクトづくりを目指す際に参考にしていただけると幸いです!
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1