はじめに
自分のアーティスト公式サイトである 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;
}
body や a のような要素セレクタに対するスタイルは、各コンポーネント側へ分散させず、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-container や l-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の境界
一番判断に迷ったのは、Component と Project の境界です。
今回の基準は以下にしました。
複数の場所で使い回す前提の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の責務と影響範囲を明確にするための設計として有効でした。