Emotion과 함께 사용하기
Emotion과 함께 사용하기
Mantine은 7.0 버전 전까지 스타일링 솔루션으로 Emotion을 사용했어요. 7.0 버전에서 CSS modules로 대체되었지만, CSS modules보다 Emotion을 선호한다면 여전히 Mantine과 함께 Emotion을 사용할 수 있어요.
createStyles 함수와 sx, styles props는 6.x 버전의 같은 기능과 다르게 동작한다는 점에 주의하세요. 6.x에서 7.x로 업그레이드할 계획이라면 마이그레이션 가이드를 따라야 해요.
@mantine/emotion 패키지는 @mantine/core 7.9.0 이상과 호환돼요. 설치 전에 모든 @mantine/* 패키지의 최신 버전을 사용하고 있는지 확인하세요.
출처: 문서
본문
주의사항과 지원
Emotion은 런타임 CSS-in-JS 라이브러리예요 – 스타일이 런타임에 생성되어 DOM에 주입돼요. 이 접근 방식에는 몇 가지 제한이 있어요:
-
제한된 서버 사이드 렌더링 지원 – app router를 사용하는 Next.js 같은 현대 프레임워크는 Emotion을 완전히 지원하지 않거나 추가 구성이 필요해요.
-
런타임 오버헤드 – 스타일이 런타임에 생성되고 주입되어 컴포넌트가 많은 페이지에서 성능 문제가 발생할 수 있어요.
-
추가 번들 크기 – 번들에
@emotion/react(minified 21.2kB),@mantine/emotion(minified 약 2kB) 및 컴포넌트에서 사용하는 모든 스타일이 포함돼요.
@mantine/emotion 패키지는 다음 프레임워크에서 사용할 수 있어요:
-
기본 설정의 Vite와 CRA
-
패키지가 제공하는 서버 사이드 렌더링용 추가 설정이 있는 Next.js pages router
-
Emotion이 제공하는 서버 사이드 렌더링용 추가 설정이 있는 Next.js app router
-
서버 사이드 렌더링이 필요 없는 다른 프레임워크(기본 설정)
공식 지원 없음(패키지를 사용할 수는 있지만 테스트되지 않았고 문서도 제공되지 않음):
-
React Router
-
Gatsby
-
Redwood
-
서버 사이드 렌더링이 있는 다른 프레임워크
Emotion은 새 프로젝트에는 권장되지 않는다는 점에 주의하세요. Mantine으로 새 프로젝트를 시작한다면 CSS modules를 대신 고려해 보세요.
Vite와 함께 사용
의존성을 설치해요:
yarn add @mantine/emotion @emotion/react @emotion/cache @emotion/serialize @emotion/utils
src 디렉터리에 emotion.d.ts 파일을 만들어 sx와 styles props에 대한 타입 지원을 추가해요:
import '@mantine/core';
import type { EmotionStyles, EmotionSx } from '@mantine/emotion';
declare module '@mantine/core' {
export interface BoxProps {
sx?: EmotionSx;
styles?: EmotionStyles;
}
}
애플리케이션을 MantineEmotionProvider로 감싸고 MantineProvider에 emotionTransform을 추가해요:
import '@mantine/core/styles.css';
import { MantineProvider } from '@mantine/core';
import {
emotionTransform,
MantineEmotionProvider,
} from '@mantine/emotion';
export default function App() {
return (
<MantineEmotionProvider>
<MantineProvider stylesTransform={emotionTransform}>
App
</MantineProvider>
</MantineEmotionProvider>
);
}
완료! 이제 애플리케이션에서 sx, styles props와 createStyles를 사용할 수 있어요:
import { Box } from '@mantine/core';
function Demo() {
return (
<Box
sx={(theme, u) => ({
padding: 40,
[u.light]: {
backgroundColor: theme.colors.blue[0],
color: theme.colors.blue[9],
'&:hover': {
backgroundColor: theme.colors.blue[1],
},
},
})}
>
Box with emotion sx prop
</Box>
);
}
Next.js pages router와 함께 사용
의존성을 설치해요:
yarn add @mantine/emotion @emotion/react @emotion/cache @emotion/serialize @emotion/utils @emotion/server
emotion 폴더를 만들고 cache.ts와 emotion.d.ts 파일을 넣어요.
cache.ts 파일:
import createCache from '@emotion/cache';
export const emotionCache = createCache({ key: 'css' });
emotion.d.ts 파일:
import '@mantine/core';
import type { EmotionStyles, EmotionSx } from '@mantine/emotion';
declare module '@mantine/core' {
export interface BoxProps {
sx?: EmotionSx;
styles?: EmotionStyles;
}
}
pages/_document.tsx 파일에 다음 내용을 추가해요:
import NextDocument, {
Head,
Html,
Main,
NextScript,
} from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import { ColorSchemeScript } from '@mantine/core';
import { createGetInitialProps } from '@mantine/emotion';
// Import cache created in the previous step
import { emotionCache } from '../emotion/cache';
export default function Document() {
return (
<Html lang="en">
<Head>
<ColorSchemeScript />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
const stylesServer = createEmotionServer(emotionCache);
Document.getInitialProps = createGetInitialProps(
NextDocument,
stylesServer
);
pages/_app.tsx 파일에 MantineEmotionProvider와 emotionTransform을 추가해요:
import '@mantine/core/styles.css';
import Head from 'next/head';
import { MantineProvider } from '@mantine/core';
import {
emotionTransform,
MantineEmotionProvider,
} from '@mantine/emotion';
import { emotionCache } from '../emotion/cache';
export default function App({ Component, pageProps }: any) {
return (
<>
<Head>
<title>Mantine Template</title>
</Head>
<MantineEmotionProvider cache={emotionCache}>
<MantineProvider stylesTransform={emotionTransform}>
<Component {...pageProps} />
</MantineProvider>
</MantineEmotionProvider>
</>
);
}
완료! 이제 애플리케이션에서 sx, styles props와 createStyles를 사용할 수 있어요:
import { Box } from '@mantine/core';
function Demo() {
return (
<Box
sx={(theme, u) => ({
padding: 40,
[u.light]: {
backgroundColor: theme.colors.blue[0],
color: theme.colors.blue[9],
'&:hover': {
backgroundColor: theme.colors.blue[1],
},
},
})}
>
Box with emotion sx prop
</Box>
);
}
Next.js app router와 함께 사용
전체 설정이 담긴 예시 저장소 보기
의존성을 설치해요:
yarn add @mantine/emotion @emotion/react @emotion/cache @emotion/serialize @emotion/utils @emotion/server
다음 내용으로 app/emotion.d.ts 파일을 만들어요:
import '@mantine/core';
import type { EmotionStyles, EmotionSx } from '@mantine/emotion';
declare module '@mantine/core' {
export interface BoxProps {
sx?: EmotionSx;
styles?: EmotionStyles;
}
}
다음 내용으로 app/EmotionRootStyleRegistry.tsx 파일을 만들어요:
'use client';
import { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
export function RootStyleRegistry({
children,
}: {
children: React.ReactNode;
}) {
const [{ cache, flush }] = useState(() => {
const cache = createCache({ key: 'my' });
cache.compat = true;
const prevInsert = cache.insert;
let inserted: string[] = [];
cache.insert = (...args) => {
const serialized = args[1];
if (cache.inserted[serialized.name] === undefined) {
inserted.push(serialized.name);
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const names = flush();
if (names.length === 0) return null;
let styles = '';
for (const name of names) {
styles += cache.inserted[name];
}
return (
<style
data-emotion={`${cache.key} ${names.join(' ')}`}
dangerouslySetInnerHTML={{ __html: styles }}
/>
);
});
return <CacheProvider value={cache}>{children}</CacheProvider>;
}
app/layout.tsx에 RootStyleRegistry, MantineEmotionProvider, emotionTransform을 추가해요. 대략 이렇게 보일 거예요:
import '@mantine/core/styles.css';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import {
emotionTransform,
MantineEmotionProvider,
} from '@mantine/emotion';
import { RootStyleRegistry } from './EmotionRootStyleRegistry';
export const metadata = {
title: 'Mantine Next.js template',
description: 'I am using Mantine with Next.js!',
};
export default function RootLayout({ children }: { children: any }) {
return (
<RootStyleRegistry>
<html lang="en">
<head>
<ColorSchemeScript />
</head>
<body>
<MantineEmotionProvider>
<MantineProvider stylesTransform={emotionTransform}>
{children}
</MantineProvider>
</MantineEmotionProvider>
</body>
</html>
</RootStyleRegistry>
);
}
완료! 이제 애플리케이션에서 sx, styles props와 createStyles를 사용할 수 있어요. sx, styles 또는 createStyles를 사용하는 대부분의 컴포넌트에는 'use client'가 필요하다는 점에 주의하세요:
'use client';
import { Box } from '@mantine/core';
export default function HomePage() {
return (
<Box
sx={(theme, u) => ({
padding: 40,
[u.light]: {
backgroundColor: theme.colors.blue[0],
color: theme.colors.blue[9],
'&:hover': {
backgroundColor: theme.colors.blue[1],
},
},
})}
>
Box with emotion sx prop
</Box>
);
}
sx prop
위 설정으로 모든 Mantine 컴포넌트에서 sx prop을 사용할 수 있어요. sx prop은 컴포넌트의 루트 요소에 스타일을 추가할 수 있게 해줘요. 스타일 객체 또는 theme와 utilities를 받아 스타일 객체를 반환하는 함수를 인자로 받아요:
import { Box, Button } from '@mantine/core';
function Demo() {
return (
<>
<Box sx={{ padding: 10, color: 'red' }}>Box with object sx</Box>
<Button
sx={(theme, u) => ({
padding: 10,
[u.light]: {
backgroundColor: theme.colors.blue[0],
color: theme.colors.blue[9],
'&:hover': {
backgroundColor: theme.colors.blue[1],
},
},
[u.dark]: {
backgroundColor: theme.colors.blue[9],
color: theme.colors.blue[0],
'&:hover': {
backgroundColor: theme.colors.blue[8],
},
},
})}
>
Button with function sx
</Button>
</>
);
}
mergeSx 함수
mergeSx 함수를 사용해 여러 sx prop을 하나로 병합할 수 있어요. 커스텀 컴포넌트에 제공된 sx prop을 그 컴포넌트 자신의 sx와 병합할 때 유용해요:
import { Box } from '@mantine/core'
import { EmotionSx, mergeSx } from '@mantine/emotion'
interface MyCustomBoxProps {
sx?: EmotionSx
}
function MyCustomBox({ sx }: MyCustomBoxProps) {
return (
<Box sx={mergeSx((theme) => ({ ... }), sx)}>...</Box>
)
}
function App() {
return (
<MyCustomBox sx={(theme) => ({ ... })} />
)
}
styles prop
styles prop은 sx prop과 비슷하게 동작하지만, Styles API 표에 지정된 컴포넌트의 모든 중첩 요소에 스타일을 추가할 수 있어요. styles prop은 styles 객체의 객체 또는 theme, 컴포넌트 props, utilities를 받아 styles 객체를 반환하는 함수를 인자로 받아요:
import { Button } from '@mantine/core';
function Demo() {
return (
<Button
styles={(theme, { color }, u) => ({
root: {
padding: 10,
backgroundColor: theme.colors[color || 'blue'][7],
color: theme.white,
'&:hover': {
backgroundColor: theme.colors[color || 'blue'][8],
},
},
label: {
[u.light]: {
border: `1px solid ${theme.black}`,
},
[u.dark]: {
border: `1px solid ${theme.white}`,
},
},
})}
>
Button with styles prop
</Button>
);
}
theme의 styles
Styles API를 사용하는 Mantine 컴포넌트에 Emotion의 styles prop으로 스타일을 추가할 수 있어요. 타입 충돌을 피하기 위해 Component.extend 메서드를 사용하지 말고 컴포넌트 설정 객체를 직접 전달해야 한다는 점에 주의하세요.
import { createTheme, MantineTheme, TextProps } from '@mantine/core';
import { EmotionHelpers } from '@mantine/emotion';
export const theme = createTheme({
components: {
Text: {
styles: (
theme: MantineTheme,
_props: TextProps,
u: EmotionHelpers
) => ({
root: {
[u.light]: {
color: theme.colors.blue[7],
},
},
}),
},
},
});
createStyles
createStyles 함수는 Emotion으로 스타일을 생성하는 함수를 받아요. 이 함수는 다음 데모들에서 더 자세히 설명할 3개의 인자를 받아요:
-
theme– Mantine theme 객체 -
params–useStyles훅에서 함수에 전달할 수 있는 추가 매개변수를 담은 객체 -
u– selector를 생성하는 utilities 객체
createStyles 함수는 주어진 스타일을 사용하는 컴포넌트에서 호출해야 하는 useStyles 훅을 반환해요:
import { createStyles } from '@mantine/emotion';
const useStyles = createStyles((theme, _, u) => ({
wrapper: {
maxWidth: 400,
width: '100%',
height: 180,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 'auto',
marginRight: 'auto',
borderRadius: theme.radius.sm,
// Use light and dark selectors to change styles based on color scheme
[u.light]: {
backgroundColor: theme.colors.gray[1],
},
[u.dark]: {
backgroundColor: theme.colors.dark[5],
},
// Reference theme.breakpoints in smallerThan and largerThan functions
[u.smallerThan('sm')]: {
// Child reference in nested selectors via ref
[`& .${u.ref('child')}`]: {
fontSize: theme.fontSizes.xs,
},
},
},
child: {
// Assign selector to a ref to reference it in other styles
ref: u.ref('child'),
padding: theme.spacing.md,
borderRadius: theme.radius.sm,
boxShadow: theme.shadows.md,
[u.light]: {
backgroundColor: theme.white,
color: theme.black,
},
[u.dark]: {
backgroundColor: theme.colors.dark[8],
color: theme.white,
},
},
}));
function Demo() {
const { classes } = useStyles();
return (
<div className={classes.wrapper}>
<p className={classes.child}>createStyles demo</p>
</div>
);
}
Pseudo-classes
Sass 같은 css 전처리기에서처럼 pseudo-classes를 추가할 수 있어요:
import { createStyles } from '@mantine/emotion';
const useStyles = createStyles((theme) => ({
button: {
color: theme.white,
backgroundColor: theme.colors.blue[6],
border: 0,
borderRadius: theme.radius.md,
padding: `${theme.spacing.sm} ${theme.spacing.lg}`,
cursor: 'pointer',
margin: theme.spacing.md,
// Use pseudo-classes just like you would in Sass
'&:hover': {
backgroundColor: theme.colors.blue[9],
},
'&:not(:first-of-type)': {
backgroundColor: theme.colors.violet[6],
// pseudo-classes can be nested
'&:hover': {
backgroundColor: theme.colors.violet[9],
},
},
},
}));
function Demo() {
const { classes } = useStyles();
return (
<div>
<button className={classes.button}>First</button>
<button className={classes.button}>Second</button>
<button className={classes.button}>Third</button>
</div>
);
}
Styles 매개변수
createStyles 함수의 두 번째 인자로 원하는 만큼 매개변수를 받을 수 있고, 나중에 이 매개변수들을 useStyles 훅의 인자로 전달해야 해요:
import { createStyles } from '@mantine/emotion';
interface ButtonProps {
color: 'blue' | 'violet';
radius: number;
}
const useStyles = createStyles((theme, { color, radius }: ButtonProps) => ({
button: {
color: theme.white,
backgroundColor: theme.colors[color][6],
borderRadius: radius,
padding: theme.spacing.md,
margin: theme.spacing.md,
border: 0,
cursor: 'pointer',
},
}));
function Button({ color, radius }: ButtonProps) {
const { classes } = useStyles({ color, radius });
return (
<button className={classes.button}>
{color} button with {radius} radius
</button>
);
}
function Demo() {
return (
<>
<Button color="blue" radius={5} />
<Button color="violet" radius={50} />
</>
);
}
컴포지션과 중첩 selector
createStyles는 스코프된 클래스 이름을 생성하므로 정적 selector를 얻으려면 selector에 대한 참조를 만들어야 해요. u.ref 함수로 정적 selector를 할당해요:
import { createStyles } from '@mantine/emotion';
const useStyles = createStyles((theme, _, u) => ({
button: {
// assign reference to selector
ref: u.ref('button'),
// and add any other properties
backgroundColor: theme.colors.blue[6],
color: theme.white,
padding: `${theme.spacing.sm} ${theme.spacing.lg}`,
borderRadius: theme.radius.md,
cursor: 'pointer',
border: 0,
},
container: {
display: 'flex',
justifyContent: 'center',
padding: theme.spacing.xl,
[u.light]: {
backgroundColor: theme.colors.gray[1],
},
[u.dark]: {
backgroundColor: theme.colors.dark[8],
},
// reference button with nested selector
[`&:hover .${u.ref('button')}`]: {
backgroundColor: theme.colors.violet[6],
},
},
}));
function Demo() {
const { classes } = useStyles();
return (
<div className={classes.container}>
<button className={classes.button}>Hover container to change button color</button>
</div>
);
}
클래스 병합 (cx 함수)
클래스 이름을 병합하려면 cx 함수를 사용해요. clsx 패키지와 같은 API를 가져요.
!important: classnames나 clsx 같은 외부 라이브러리를 createStyles 함수로 만든 클래스 이름과 함께 사용하지 마세요 – 스타일 충돌이 발생할 수 있어요.
import { useState } from 'react';
import { createStyles } from '@mantine/emotion';
const useStyles = createStyles((theme, _, u) => ({
button: {
border: 0,
borderRadius: theme.radius.md,
padding: theme.spacing.md,
cursor: 'pointer',
margin: theme.spacing.md,
lineHeight: 1,
[u.light]: {
backgroundColor: theme.colors.gray[1],
},
[u.dark]: {
backgroundColor: theme.colors.dark[5],
},
},
active: {
color: theme.white,
[u.light]: {
backgroundColor: theme.colors.blue[6],
},
[u.dark]: {
backgroundColor: theme.colors.blue[8],
},
},
}));
function Demo() {
const [active, setActive] = useState(0);
const { classes, cx } = useStyles();
return (
<div>
<button
className={cx(classes.button, { [classes.active]: active === 0 })}
onClick={() => setActive(0)}
type="button"
>
First
</button>
<button
className={cx(classes.button, { [classes.active]: active === 1 })}
onClick={() => setActive(1)}
type="button"
>
Second
</button>
</div>
);
}
미디어 쿼리
Sass처럼 중첩된 미디어 쿼리를 사용할 수 있어요. 쿼리 본문 안에서 MantineProvider로 정의된 theme.breakpoints나 정적 값을 사용할 수 있어요:
import { em, getBreakpointValue } from '@mantine/core';
import { createStyles } from '@mantine/emotion';
const useStyles = createStyles((theme, _, u) => ({
container: {
height: 100,
backgroundColor: theme.colors.blue[6],
// Media query with value from theme
[`@media (max-width: ${em(getBreakpointValue(theme.breakpoints.xl, theme.breakpoints) - 1)})`]: {
backgroundColor: theme.colors.pink[6],
},
// Simplify media query writing with theme functions
[u.smallerThan('lg')]: {
backgroundColor: theme.colors.yellow[6],
},
// Static media query
[`@media (max-width: ${em(800)})`]: {
backgroundColor: theme.colors.orange[6],
},
},
}));
function Demo() {
const { classes } = useStyles();
return <div className={classes.container} />;
}
Keyframes
import { createStyles, keyframes } from '@mantine/emotion';
// Export animation to reuse it in other components
export const bounce = keyframes({
'from, 20%, 53%, 80%, to': { transform: 'translate3d(0, 0, 0)' },
'40%, 43%': { transform: 'translate3d(0, -30px, 0)' },
'70%': { transform: 'translate3d(0, -15px, 0)' },
'90%': { transform: 'translate3d(0, -4px, 0)' },
});
const useStyles = createStyles((theme) => ({
container: {
textAlign: 'center',
padding: theme.spacing.xl,
animation: `${bounce} 3s ease-in-out infinite`,
},
}));
function Demo() {
const { classes } = useStyles();
return <div className={classes.container}>Keyframes demo</div>;
}
Utilities
sx, styles, createStyles 콜백 함수는 selector를 생성하는 utilities가 담긴 u 객체를 받아요. u 객체는 다음 속성을 포함해요:
const u = {
light: '[data-mantine-color-scheme="light"] &',
dark: '[data-mantine-color-scheme="dark"] &',
rtl: '[dir="rtl"] &',
ltr: '[dir="ltr"] &',
notRtl: '[dir="ltr"] &',
notLtr: '[dir="rtl"] &',
ref: getStylesRef,
smallerThan: (breakpoint: MantineBreakpoint | number) =>
`@media (max-width: ${em(getBreakpointValue(theme, breakpoint) - 0.1)})`,
largerThan: (breakpoint: MantineBreakpoint | number) =>
`@media (min-width: ${em(getBreakpointValue(theme, breakpoint))})`,
};
ref를 제외한 모든 utilities는 styles 객체의 selector로 사용할 수 있어요:
const styles = {
root: {
[u.dark]: { color: 'white' },
[u.rtl]: { padding: 10 },
[u.smallerThan('md')]: { lineHeight: 20 },
},
};