1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React + Sass構成の個人サイトにFLOCSSを導入してCSSの責務を整理した

1
Posted at

はじめに

自分のアーティスト公式サイトである 7g3.netのスタイル設計を、FLOCSSベースに再整理しました。

構成は以下の通りです。

React
TypeScript
Vite
Sass

個人サイトではありますが、Works、About、Contact、Navigationなどのセクションが増えるにつれて、CSSの責務が曖昧になってきました。

具体的には、以下のような問題がありました。

- 共通パーツとページ固有スタイルの境界が曖昧になる
- 似たような余白・カード・ボタンのスタイルが増える
- クラス名の命名基準が揺れる
- 修正時に影響範囲を把握しづらい
- レスポンシブ対応の記述が散らばる

そこで、Sassのファイル構成とクラス命名をFLOCSSベースに整理しました。

FLOCSSを採用した理由

FLOCSSは、CSSを以下のようなレイヤーに分けて管理する設計手法です。

Foundation
Layout
Object
  Component
  Project
  Utility

今回のサイトでは、厳密な大規模開発向けの運用というよりも、個人サイトでも継続的にメンテナンスしやすいCSS構造を作ることを目的に導入しました。

特に重視したのは以下です。

- グローバルな基盤スタイルを分離する
- レイアウト用クラスと装飾用クラスを分ける
- 再利用可能なUIとページ固有UIを分ける
- クラス名から責務を判断できるようにする

ディレクトリ構成

最終的なスタイル周りの構成は以下のようにしました。

src/styles/
  main.scss

  foundation/
    _tokens.scss
    _reset.scss
    _base.scss

  layout/
    _shell.scss
    _container.scss
    _section.scss

  object/
    component/
      _button.scss
      _card.scss
      _chip.scss
      _form.scss
      _section-title.scss
      _skip-link.scss

    project/
      _navigation.scss
      _hero.scss
      _works.scss
      _now-playing.scss
      _about.scss
      _contact.scss
      _collaboration-form.scss
      _footer.scss

    utility/
      _visually-hidden.scss
      _motion.scss

main.scss では、各レイヤーを明示的に読み込んでいます。

@use './foundation/tokens';
@use './foundation/reset';
@use './foundation/base';

@use './layout/shell';
@use './layout/container';
@use './layout/section';

@use './object/component/skip-link';
@use './object/component/button';
@use './object/component/chip';
@use './object/component/card';
@use './object/component/form';
@use './object/component/section-title';

@use './object/project/navigation';
@use './object/project/hero';
@use './object/project/works';
@use './object/project/now-playing';
@use './object/project/about';
@use './object/project/contact';
@use './object/project/collaboration-form';
@use './object/project/footer';

@use './object/utility/visually-hidden';
@use './object/utility/motion';

エントリーポイントでは、この main.scss のみを読み込む構成にしています。

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './styles/main.scss'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

スタイルの入口を1つにすることで、CSSの読み込み順と依存関係を把握しやすくしました。

Foundation

foundation には、サイト全体に関わる基本スタイルを配置しています。

foundation/
  _tokens.scss
  _reset.scss
  _base.scss

tokens

色、フォント、角丸などのデザイントークンはCSSカスタムプロパティとして定義しました。

:root {
  --paper: #fbfaf7;
  --paper-2: #f2f0ea;
  --ink: #161616;
  --sub: rgba(22, 22, 22, .68);
  --thin: rgba(22, 22, 22, .14);
  --hair: rgba(22, 22, 22, .08);
  --accent: #ff5c8a;
  --accent-readable: #a91545;
  --accent-2: #6bd7e8;
  --radius: 18px;
  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Noto Sans JP", "Segoe UI", sans-serif;
  --mono: ui-monospace, "SFMono-Regular", "Cascadia Code", "Roboto Mono", "Noto Sans Mono", monospace;
}

これにより、各コンポーネントで直接色や角丸を指定する必要が減りました。

border: 1px solid var(--thin);
border-radius: var(--radius);
color: var(--ink);

デザインの基準値を foundation に寄せたことで、サイト全体の見た目を一貫させやすくなりました。

base

サイト全体に効く背景、フォント、リンク、フォーカススタイルなどは base に配置しています。

html {
  scroll-behavior: smooth;
  scroll-padding-top: 80px;
}

body {
  color: var(--ink);
  background:
    linear-gradient(90deg, rgba(22, 22, 22, .028) 1px, transparent 1px) 0 0 / 72px 72px,
    linear-gradient(var(--paper), var(--paper-2));
  font-family: var(--sans);
  font-feature-settings: "palt";
  letter-spacing: -.01em;
  overflow-x: hidden;
  overflow-x: clip;
}

a {
  color: inherit;
  text-decoration: none;
}

a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

bodya のような要素セレクタに対するスタイルは、各コンポーネント側へ分散させず、Foundationに集約しました。

Layout

layout には、見た目の装飾ではなく、ページ全体の構造に関わるクラスを配置しています。

layout/
  _shell.scss
  _container.scss
  _section.scss

例として、サイト全体のコンテナ幅は l-container として定義しました。

.l-container {
  width: min(1180px, calc(100% - 36px));
  margin: 0 auto;
}

@media (max-width: 560px) {
  .l-container {
    width: min(100% - 28px, 1180px);
  }
}

セクション単位の余白は l-section にまとめています。

.l-section {
  padding: 78px 0;
  border-bottom: 1px solid var(--hair);
}

@media (max-width: 560px) {
  .l-section {
    padding: 58px 0;
  }
}

React側では、以下のように利用しています。

export function HomePage() {
  return (
    <main className="l-container" id="main-content" tabIndex={-1}>
      <HeroSection />
      <WorksSection />
      <NowPlayingSection />
      <section className="l-section" id="about">
        <div className="p-about-layout">
          <AboutSection />
          <TimelineSection />
        </div>
      </section>
      <ContactSection />
    </main>
  )
}

l-containerl-section を分離したことで、ページ全体の幅や余白をセクション固有のCSSから切り離せました。

Component

component には、複数箇所で再利用する小さなUIを配置しています。

object/component/
  _button.scss
  _card.scss
  _form.scss
  _chip.scss
  _section-title.scss

たとえば、ボタンは c-button として定義しています。

.c-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 42px;
  padding: 0 16px;
  border: 1px solid var(--thin);
  border-radius: 999px;
  background: rgba(255, 255, 255, .4);
  color: inherit;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .05em;
  transition: transform .16s ease, background .16s ease;

  &:hover {
    transform: translateY(-1px);
    background: rgba(255, 255, 255, .72);
  }
}

.c-button--primary {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);

  &:hover {
    background: #2a2a2a;
  }
}

React側では、LinkButton コンポーネントからこのクラスを利用します。

export function linkButtonClassName(
  variant: 'primary' | 'secondary' = 'secondary',
  className?: string,
) {
  return classNames('c-button', variant === 'primary' && 'c-button--primary', className)
}

このように、ReactコンポーネントとFLOCSSの Component レイヤーを対応させることで、UIの再利用単位が明確になりました。

Project

project には、サイト固有のセクションや画面単位のスタイルを配置しています。

object/project/
  _navigation.scss
  _hero.scss
  _works.scss
  _about.scss
  _contact.scss

たとえば、Heroセクションは p-hero として定義しています。

.p-hero {
  min-height: 74vh;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  align-items: end;
  gap: clamp(32px, 8vw, 100px);
  padding: clamp(56px, 9vw, 106px) 0 76px;
  border-bottom: 1px solid var(--hair);
}

.p-hero__title {
  margin: 28px 0 0;
  font-size: clamp(88px, 20vw, 230px);
  line-height: .76;
  letter-spacing: -.115em;
  font-weight: 860;
}

Worksセクションのカードは p-release-card として管理しています。

.p-release-card {
  min-height: 390px;
  display: grid;
  grid-template-columns: minmax(260px, 300px) minmax(0, 1fr);
  overflow: hidden;
  border: 1px solid var(--thin);
  background: rgba(255, 255, 255, .44);
  border-radius: var(--radius);
}

.p-release-card__cover-frame {
  aspect-ratio: 1;
  width: 100%;
  border-right: 1px solid var(--thin);
  background: rgba(255, 255, 255, .42);
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
}

React側では、以下のようになります。

export function ReleaseCard({ release }: ReleaseCardProps) {
  return (
    <article className="p-release-card" data-ui="work-card" id={release.id}>
      <div className="p-release-card__cover-frame" data-ui="cover-frame">
        <ResponsiveImage
          className="p-release-card__cover-image"
          image={release.image}
          loading="lazy"
          decoding="async"
        />
      </div>

      <div className="p-release-card__info">
        <div>
          <Badge variant="tagline">{release.label}</Badge>
          <h3>{release.title}</h3>
          <p>{release.description}</p>
          <CreditBlock items={release.metadata} />
        </div>
      </div>
    </article>
  )
}

p-release-card はこのサイトのWorks表示に依存したUIなので、Component ではなく Project として扱いました。

Utility

utility には、単一目的の補助クラスやグローバルな制御を配置しています。

object/utility/
  _visually-hidden.scss
  _motion.scss

たとえば、モーションを抑制する設定は以下のように分離しています。

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    transition-duration: .01ms !important;
  }
}

アクセシビリティやユーザー設定に関わる補助的な処理は、個別コンポーネントに混ぜず、Utilityとして分離しました。

導入してよかったこと

1. CSSの責務が明確になった

FLOCSS導入前は、スタイルの置き場所が曖昧になりやすい状態でした。

導入後は、以下のように判断できます。

全体の基盤       -> foundation
ページの構造     -> layout
再利用するUI     -> component
サイト固有のUI   -> project
補助クラス       -> utility

この分類により、新しくCSSを書くときの判断コストが下がりました。

2. クラス名から影響範囲を推測しやすくなった

FLOCSSのプレフィックスにより、クラス名から責務を判断できます。

<main class="l-container">
  <section class="l-section">
    <div class="p-works">
      <article class="p-release-card">
        <a class="c-button c-button--primary">LISTEN</a>
      </article>
    </div>
  </section>
</main>

この例では、以下のように判断できます。

l-container           -> レイアウト用
l-section             -> セクション余白用
p-works               -> Worksセクション固有
p-release-card        -> リリースカード固有
c-button              -> 再利用可能なボタン
c-button--primary     -> ボタンのmodifier

CSSを開く前に影響範囲を推測できるため、修正時のリスクを下げられます。

3. Reactコンポーネントとの対応関係が整理された

ReactではUIをコンポーネント単位で分割します。

FLOCSSを導入したことで、ReactコンポーネントとCSSの責務を対応させやすくなりました。

LinkButton.tsx      -> c-button
HeroSection.tsx     -> p-hero
WorksSection.tsx    -> p-works
ReleaseCard.tsx     -> p-release-card
HomePage.tsx        -> l-container / l-section

すべてをCSS ModulesやCSS-in-JSに寄せるのではなく、Sass + グローバルクラスでも、責務を明確にすれば十分に管理しやすくなると感じました。

4. デザイン変更の影響範囲を抑えやすくなった

たとえば、サイト全体の角丸を変更したい場合は、--radius を変更します。

:root {
  --radius: 18px;
}

セクション全体の余白を変えたい場合は、l-section を変更します。

.l-section {
  padding: 78px 0;
}

Heroだけを調整したい場合は、p-hero を変更します。

.p-hero {
  padding: clamp(56px, 9vw, 106px) 0 76px;
}

このように、変更したい対象に応じて編集するファイルが明確になりました。

5. レスポンシブ対応を整理しやすくなった

レスポンシブ対応では、全体に関わる調整とセクション固有の調整が混ざりやすくなります。

FLOCSS導入後は、以下のように分けられます。

コンテナ幅の調整       -> layout/_container.scss
セクション余白の調整   -> layout/_section.scss
Hero固有の調整         -> project/_hero.scss
Works固有の調整        -> project/_works.scss

たとえば、Worksカードのカラム変更は p-release-card 側に閉じ込めます。

@media (max-width: 920px) {
  .p-works,
  .p-release-card {
    grid-template-columns: 1fr;
  }

  .p-release-card__cover-frame {
    border-right: 0;
    border-bottom: 1px solid var(--thin);
  }
}

レイアウト全体の調整と、セクション固有の調整を分離できたことで、レスポンシブ対応の見通しが良くなりました。

判断に迷った点

ComponentとProjectの境界

一番判断に迷ったのは、ComponentProject の境界です。

今回の基準は以下にしました。

複数の場所で使い回す前提のUI
  -> Component

特定のセクションやコンテンツに依存するUI
  -> Project

たとえば、ボタンはサイト内の複数箇所で使うため c-button にしています。

.c-button {
  display: inline-flex;
}

一方で、リリースカードはWorks表示に強く依存しているため、p-release-card としています。

.p-release-card {
  display: grid;
}

最初からすべてを汎用化しようとすると、逆に設計が複雑になります。
そのため、最初は Project に置き、再利用したくなった段階で Component に切り出す方針にしました。

個人サイトでもCSS設計は有効だった

FLOCSSは大規模サイトやチーム開発向けの印象が強いですが、個人サイトでも効果がありました。

特に、個人サイトは以下のような特徴があります。

- 作品や実績が後から増える
- しばらく時間を置いてから修正することがある
- デザインの微調整を継続的に行う
- ページ数は少なくても、セクション単位では増えていく

そのため、最初から最低限のCSS設計を入れておくことで、後からの修正がかなり楽になります。

まとめ

React + TypeScript + Vite + Sass構成の個人サイトにFLOCSSを導入したことで、CSSの責務を整理できました。

主なメリットは以下です。

- CSSの置き場所が明確になった
- クラス名から責務を判断できるようになった
- Reactコンポーネントとの対応関係が整理された
- デザイン変更時の影響範囲を抑えやすくなった
- レスポンシブ対応の記述を整理しやすくなった
- 個人サイトでも継続的にメンテナンスしやすくなった

小規模なサイトであっても、CSSの責務が曖昧なまま成長すると、後から修正しづらくなります。

今回のような個人サイトでは、厳密すぎる設計ではなく、最低限のルールとしてFLOCSSを導入するくらいがちょうどよいと感じました。

FLOCSSは、単にクラス名を整理するためのものではなく、CSSの責務と影響範囲を明確にするための設計として有効でした。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?