Emotion の機能を使って、こんな感じで Utility First な記述を試してみました。
<div css={limitWidth()}>
<main
css={[
minHeight(640),
pcOnly([paper, my(4)]),
mbOnly(bgColor("#fafafa")),
contentsStyle,
]}
>
<section css={[responsive({ mb: p(2), pc: p(3) })]}>
<h1 css={[typography.h1]}>ああああ</h1>
<p>
Irofa nifofedo tirinuruwo wagayo tarezo tunenaram uwino okuyama kefu
koete asaki yumemisi wefimo sezu
</p>
</section>
</main>
</div>
(共通化は漏れが起きやすいので、こういう小さなユーティリティの組み合わせが楽なんですよね... )
利用しているライブラリのバージョン
パッケージ | バージョン |
---|---|
@emotion/react | 11.9.3 |
next | 12.1.6 |
react, react-dom | 18.1.0 |
Emotion の css
Prop について
まず、css
Prop の型は Interpolation<Theme>
型になっています。そして、Interpolation
は以下のように定義されています。
export type Interpolation<Props> =
| InterpolationPrimitive
| ArrayInterpolation<Props>
| FunctionInterpolation<Props>
css タグリテラル(基本形)
css``
の式の結果は、InterpolationPrimitive
型(のサブタイプ)になるので、
<div css={css`background-color: red;`}></div>
と記述できるわけです。
テーマを使うこともできる。
1つ飛ばして、次は3つ目の FunctionInterpolation
を見てみましょう。
export interface FunctionInterpolation<Props> {
(props: Props): Interpolation<Props>
}
となっているので、 Theme
(つまり、Emotionで宣言したテーマ)を引数に取ることが出来ることが分かります。
<div
css={(theme) =>
css`background-color: ${theme.colors.primary};`
}
>
ああああ
</div>
配列も指定できる。
ArrayInterpolation
を見てみましょう。これがミソです。
export interface ArrayInterpolation<Props>
extends Array<Interpolation<Props>> {}
となっているので、配列の形で渡すことが出来ます。
<div
css={[
css`color: white;`,
// 1) テーマを引数に取る指定も含められる
(theme) => css`background-color: ${theme.colors.primary};`,
// 2) && etc. による条件付き指定も可
selected && css`background-color: red;`,
]}
>
いいいい
</div>
コメントで記述したように、1) テーマを引数にとることで、テーマを利用することも出来ますし、2) &&
演算子等による、条件つきの指定もできるようになっています。(もちろん三項演算子も使用可)
&&
が使えるお陰で、clsx によるクラス指定みたいに、シンプルな表記で 適用/不適用 を制御できるのが嬉しいですね。
これを応用すれば...
勘の良い皆さまはもうお気づきでしょう。この配列を使った指定方法を取れば、Interpolation<Theme>
型のを返す関数として切り出し、それらを呼び出して並べることで、スタイルを合成することができます。
これが、emotion で Utility First 的な記述が出来るカラクリです。
ユーティリティー関数群
最初に示した記述に使われているUtility群は、このように定義されています。
type CSSInterpolation = Parameters<typeof css>[number];
const breakpoint = "640px";
export const pcOnly = (s: CSSInterpolation) =>
css`
@media screen and (min-width: ${breakpoint}) {
${s}
}
`;
export const mbOnly = (s: CSSInterpolation) =>
css`
@media screen and (max-width: ${breakpoint}) {
${s}
}
`;
export const spacing = (value: number | string) =>
typeof value === "string" ? value : `${8 * value}px`;
// system
export const minHeight = (px: number) =>
css`
min-height: ${px}px;
`;
export const my = (units: number | string) =>
css`
margin-top: ${spacing(units)};
margin-bottom: ${spacing(units)};
`;
export const mx = (units: number | string) =>
css`
margin-right: ${spacing(units)};
margin-left: ${spacing(units)};
`;
export const p = (units: number | string) =>
css`
padding: ${spacing(units)};
`;
// typography
export const typography = {
h1: [
css`
font-size: 2.25rem;
`,
pcOnly(css`
font-size: 3rem;
`),
],
};
// color
export const bgColor = (color: string) =>
css`
background-color: ${color};
`;
// concrete
export const limitWidth = (width: number = 960) => css`
max-width: ${width}px;
padding: 0 ${spacing(2)}; // === 16px
margin: 0 auto;
`;
export const paper = css`
background-color: white;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.02), 0 8px 32px rgba(0, 0, 0, 0.1);
`;
参考記事(公式Doc)