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 },
  },
};

더 알아보기 (Learn more)