これは何
- Next.jsではCSS Modulesとstyled-jsxがビルトインサポートされており、その2つを比較してみた記事です
- またフロントエンド強化月間 - 開発する上で知っておくべき知見を共有しようイベントへの投稿記事でもあります
作った物
Figma | コーディングした結果 |
---|---|
![]() |
![]() |
何かしらを投稿するサイトっぽい見た目で作ってみました。
(検証のために作っただけなのでモバイル対応はしていません。大変な崩れ方をします。)
GitHubで全てのコードを公開しているので良かったら見てください。
機能比較のための項目
- SCSSの導入
- CSS自体はビルトインサポートだけどSCSSはサポートされていない
- コンポーネント自体のスタイルと親から渡すスタイルを分ける
- コンポーネント自体の色や文字の大きさなど
- 親要素から渡すgridやmarginなど
- propsを渡してコンポーネントの見た目を変える
- ボタンのコンポーネント
- 塗りと線のバージョン
- 大きめサイズと小さめサイズのバージョン
- 投稿記事のコンポーネント
- 画像有りと無しのバージョン
- ボタンのコンポーネント
SCSSの導入
ビルトインサポートのスタイリングを比較していますが、なんだかんだSCSS記法は欲しくなると思います。
(疑似要素やメディアクエリなど)
CSS Modules
まずはsass
のインストール。
npm install sass
次はnext.config.js
の変更。
const path = require('path')
module.exports = {
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
},
}
なお、公式のドキュメントに記載があります。
styled-jsx
まずは@styled-jsx/plugin-sass
のインストール。
こちらでもsass
は必要なんですね。
npm install sass @styled-jsx/plugin-sass
そして.babelrc
の編集
{
"presets": [
[
"next/babel",
{
"styled-jsx": {
"plugins": ["@styled-jsx/plugin-sass"]
}
}
]
]
}
こちらはNext.js
のドキュメントには記載がありませんが、パッケージのGitHubに詳細が書いてあります。
コンポーネント自体のスタイルと親から渡すスタイルを分ける
コンポーネントそのもののスタイル(色や文字の大きさやpadding)はコンポーネントに書いて、ページ上でどうレイアウトするかは親から渡した方が良いです。
ボタンのコンポーネントと、それがヘッダーの中にあるときを例にして説明します。
CSS Modules
props
として受け取ったclassName
をbutton
のclassName
に渡せるようにすることで、marginやgrid-areaの指定などは親から渡せるようになっています。
直接は関係ありませんが、クラス名を複数持たせるにはclsx
など別途ライブラリを使う必要があります。
import { FC, ReactNode } from 'react'
import styles from '../../styles/css-modules-button.module.scss'
import clsx from 'clsx'
type InputProps = JSX.IntrinsicElements['button']
type Props = {
children: ReactNode
className?: string
size: 'm' | 's'
variant: 'fill' | 'border'
onClick?: (event: React.MouseEvent<HTMLInputElement>) => void
} & InputProps
export const CssModulesButton: FC<Props> = ({
children,
className,
size,
variant,
onClick,
}) => {
return (
<button
onClick={onClick}
className={clsx(styles.button, styles[variant], styles[size], className)}
>
{children}
</button>
)
}
親要素であるヘッダーは、普通にclassNameを指定しているだけです。
import { FC } from 'react'
import Image from 'next/image'
import { CssModulesButton } from './CssModulesButton'
import styles from '../../styles/css-modules-header.module.scss'
export const CssModulesHeader: FC = () => {
return (
<header className={styles.header}>
<div className={styles.header__inner}>
<h1 className={styles.header__logo}>
<Image src="/logotype.svg" width={116} height={24} alt="Logotype" />
</h1>
<CssModulesButton
size="m"
variant="fill"
className={styles.header__button}
onClick={() => alert('新規作成')}
>
新規作成
</CssModulesButton>
<img
src="https://source.unsplash.com/random/80x80"
width="40"
height="40"
alt="User Icon"
className={styles.header__icon}
/>
</div>
</header>
)
}
styled-jsx
ボタンのコンポーネントはCSS Modulesとほとんど同じです。
こちらは複数のクラス名を持つにしても特別な準備は必要ありません。
余談ですが、<style jsx></style>
の中に直接スタイルを書くと見通しが悪くなりすぎるので外に出すようにしています。
import { FC, ReactNode } from 'react'
import css from 'styled-jsx/css'
type InputProps = JSX.IntrinsicElements['button']
type Props = {
children: ReactNode
className?: string
size: 'm' | 's'
variant: 'fill' | 'border'
onClick?: (event: React.MouseEvent<HTMLInputElement>) => void
} & InputProps
export const StyledJSXButton: FC<Props> = ({
children,
className,
size,
variant,
onClick,
}) => {
return (
<>
<button
onClick={onClick}
className={`button ${variant} ${size} ${className}`}
>
{children}
</button>
<style jsx>{buttonStyle}</style>
</>
)
}
const buttonStyle = css`
{/* 省略 */}
`
スタイルを渡す側の記述は若干面倒です。
import { FC } from 'react'
import Image from 'next/image'
import { StyledJSXButton } from './StyledJSXButton'
import css from 'styled-jsx/css'
export const StyledJSXHeader: FC = () => {
return (
<>
<header className="header">
<div className="header__inner">
<h1 className="header__logo">
<Image src="/logotype.svg" width={116} height={24} alt="Logotype" />
</h1>
<StyledJSXButton
size="m"
variant="fill"
className={`header__button ${className}`}
onClick={() => alert('新規作成')}
>
新規作成
</StyledJSXButton>
<img
src="https://source.unsplash.com/random/80x80"
width="40"
height="40"
alt="User Icon"
className="header__icon"
/>
</div>
</header>
<style jsx>{headerStyle}</style>
{styles}
</>
)
}
const headerStyle = css`
{/* 省略 */}
`
const { className, styles } = css.resolve`
.header__button {
margin-left: auto;
}
`
css.resolve
という記法で書く必要があります。
また、スタイルを渡したいコンポーネント(ここでいうとStyledJSXButton
)のclassNameは以下のように書いて
className={`header__button ${className}`}
<style jsx>
とは別に{styles}
も記載しなければなりません。
css.resolve
で定義されているのがclassName, styles
と名前被り必至なのが微妙に悩ましいです。
propsを渡してコンポーネントの見た目を変える
またもボタンを例に出しますが、propsを渡して以下を切り替えられるようにします。
- 塗りのボタンと線のボタン
- 大きめサイズと小さいサイズ
CSS Modules
export const CssModulesButton: FC<Props> = ({
children,
className,
size,
variant,
onClick,
}) => {
return (
<button
onClick={onClick}
className={clsx(styles.button, styles[variant], styles[size], className)}
>
{children}
</button>
)
}
`scss:css-modules-button.module.scss
.button {
border-color: transparent;
border-radius: 2px;
border-style: solid;
border-width: 1px;
font-weight: bold;
padding: 7px 15px;
}
.fill {
background-color: var(--color-primary);
color: var(--color-light-text-high);
&:hover {
background-color: var(--color-primary-dark);
}
}
.border {
border-color: var(--color-primary);
color: var(--color-dark-text-medium);
&:hover {
background-color: var(--color-background);
color: var(--color-dark-text-high);
}
}
.m {
font-size: var(--font-size-body1);
}
.s {
font-size: var(--font-size-body3);
}
`
.button
のクラスは常についていて、渡ってくるpropsにあわせてstyles[variant], styles[size]
が上手く切り替わって付与されます。
また、CSS Modulesの記事を見るとstyles.className
の記法が紹介されていますが、ドットで繋げても動かないためstyles[className]
と記載する必要があります。
よくよく考えれば分かるのですが、自分はここに苦戦してなかなか実現できませんでした。
styled-jsx
例が若干悪かったかもしれません……ほぼCSS Modulesと変わらない書き方になりました。
export const StyledJSXButton: FC<Props> = ({
children,
className,
size,
variant,
onClick,
}) => {
return (
<>
<button
onClick={onClick}
className={`button ${variant} ${size} ${className}`}
>
{children}
</button>
<style jsx>{buttonStyle}</style>
</>
)
}
const buttonStyle = css`
.button {
border-color: transparent;
border-radius: 2px;
border-style: solid;
border-width: 1px;
font-weight: bold;
padding: 7px 15px;
}
.fill {
background-color: var(--color-primary);
color: var(--color-light-text-high);
&:hover {
background-color: var(--color-primary-dark);
}
}
.border {
border-color: var(--color-primary);
color: var(--color-dark-text-medium);
&:hover {
background-color: var(--color-background);
color: var(--color-dark-text-high);
}
}
.m {
font-size: var(--font-size-body1);
}
.s {
font-size: var(--font-size-body3);
}
`
本家のドキュメントにあるDynamic stylesも紹介します。
const Button = (props) => (
<button>
{ props.children }
<style jsx>{`
button {
padding: ${ 'large' in props ? '50' : '20' }px;
background: ${props.theme.background};
color: #999;
display: inline-block;
font-size: 1em;
}
`}</style>
</button>
)
今回の例では塗りのボタンと線のボタンを切り替えるのがprops ? foo : bar
だとやたら記述量が多くなったので却って原始的な作りを実施しました。
まとめ
- SCSSの導入とpropsにあわせてスタイルを変更するのはどちらもそんなに変わらない
- 親からスタイルを受け渡すのはstyled-jsxだと割と面倒
- また
css.resolve
で定義されているのがclassName, styles
なのが名前被りして悩むタイミングがある
- また
- 個人的にはこの2つならCSS Modulesを選ぶ