Building extensible themes
Building extensible themes (확장 가능한 테마 만들기)
Material UI로 확장 가능한 테마를 만드는 방법을 배워봐요.
출처: 문서
본문
Introduction (소개)
이 가이드는 Material UI로 브랜드 특화 테마를 만들 때, 이를 소비하는 여러 앱에서 쉽게 확장되고 커스터마이즈될 수 있도록 하기 위한 권장 사항을 설명해요.
Branded theme (브랜드 테마)
이것이 브랜드 특화 테마의 근원(source of truth)이에요. 색상, 타이포그래피, 간격 등을 통해 브랜드의 시각적 정체성을 나타내요.
일반적으로는 아래처럼 토큰, 컴포넌트, 브랜드 테마를 파일에서 export하는 것이 권장돼요:
import { createTheme } from '@mui/material/styles';
import type { ThemeOptions } from '@mui/material/styles';
export const brandedTokens: ThemeOptions = {
palette: {
primary: {
main: '#000000',
},
secondary: {
main: 'rgb(229, 229, 234)',
},
},
shape: {
borderRadius: 4,
},
typography: {
fontFamily:
'var(--font-primary, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif)',
},
};
export const brandedComponents: ThemeOptions['components'] = {
MuiButton: {
defaultProps: {
disableElevation: true,
},
styleOverrides: {
root: {
minWidth: 'unset',
textTransform: 'capitalize',
'&:hover': {
textDecoration: 'underline',
},
},
},
},
};
const brandedTheme = createTheme({
...brandedTokens,
components: brandedComponents,
});
export default brandedTheme;
더 최적화된 접근 방식으로, 브랜드 컴포넌트를 여러 파일로 나눌 수도 있어요. 이렇게 하면 테마의 사용자가 애플리케이션 레벨에서 필요한 것만 선택해서 import할 수 있어요.
import type { ThemeOptions } from "@mui/material/styles";
export const buttonTheme: ThemeOptions["components"] = {
MuiButtonBase: {},
MuiButton: {},
MuiIconButton: {},
};
import { buttonTheme } from './brandedButtons';
// import other branded components as needed
export const brandedTokens: ThemeOptions = {}
export default createTheme({
...brandedTokens,
components: {
...buttonTheme,
// other branded components
},
});
Application theme (애플리케이션 테마)
브랜드 테마의 사용자(consumer)는 이를 애플리케이션에서 직접 사용하거나, 특정 사용 사례에 더 잘 맞도록 확장할 수 있어요. 브랜드 버튼을 예로 들면, 사용자는 아래처럼 hover 스타일을 커스터마이즈할 수 있어요:
import { createTheme } from '@mui/material/styles';
import { brandedTokens, brandedComponents } from './brandedTheme'; // or from an npm package.
const appTheme = createTheme({
...brandedTokens,
palette: {
...brandedTokens.palette,
primary: {
main: '#1976d2',
},
},
components: {
...brandedComponents,
MuiButton: {
styleOverrides: {
root: [
// Use array syntax to preserve the branded theme styles.
brandedComponents?.MuiButton?.styleOverrides?.root,
{
'&:hover': {
transform: 'translateY(-2px)',
},
},
],
},
},
},
});
Merging branded theme (브랜드 테마 병합)
브랜드 테마를 애플리케이션 테마와 병합할 때는 palette, typography, shape 같은 토큰에는 객체 스프레드(object spread) 문법을 사용하는 것이 권장돼요.
컴포넌트에는 배열 문법을 사용해서 브랜드 테마의 variants, 상태, 의사 클래스(pseudo-class) 스타일이 보존되도록 해요.
:::warning 브랜드 테마와 애플리케이션 테마 간의 깊은 병합(deep merge)을 위해 JavaScript 함수나 유틸리티를 사용하는 것은 권장하지 않아요. 그렇게 하면 애플리케이션의 첫 렌더링에서 성능 오버헤드가 발생해요. 그 영향은 테마의 크기에 따라 달라져요. :::
Full example (전체 예제)
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import { ThemeProvider, createTheme, type ThemeOptions } from '@mui/material/styles';
/**
* Branded theme: you might want to export this as a separate file
*/
const brandedTokens: ThemeOptions = {
palette: {
primary: {
main: '#000000',
},
secondary: {
main: 'rgb(229, 229, 234)',
},
},
shape: {
borderRadius: 4,
},
typography: {
fontFamily:
'var(--font-primary, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif)',
},
shadows: [
'none',
'0 1px 2px 0 rgb(0 0 0 / 0.05)',
'0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
'0 2px 4px 0 rgb(0 0 0 / 0.06)',
'0 2px 4px -1px rgb(0 0 0 / 0.06), 0 1px 2px -1px rgb(0 0 0 / 0.04)',
'0 3px 5px -1px rgb(0 0 0 / 0.07), 0 1px 3px -1px rgb(0 0 0 / 0.05)',
'0 4px 6px -1px rgb(0 0 0 / 0.07), 0 2px 4px -1px rgb(0 0 0 / 0.05)',
'0 5px 8px -2px rgb(0 0 0 / 0.08), 0 2px 4px -1px rgb(0 0 0 / 0.05)',
'0 6px 10px -2px rgb(0 0 0 / 0.08), 0 3px 5px -2px rgb(0 0 0 / 0.06)',
'0 8px 12px -3px rgb(0 0 0 / 0.09), 0 3px 6px -2px rgb(0 0 0 / 0.06)',
'0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 7px -3px rgb(0 0 0 / 0.07)',
'0 12px 18px -4px rgb(0 0 0 / 0.11), 0 5px 9px -3px rgb(0 0 0 / 0.08)',
'0 15px 22px -4px rgb(0 0 0 / 0.12), 0 6px 11px -4px rgb(0 0 0 / 0.09)',
'0 18px 28px -5px rgb(0 0 0 / 0.13), 0 7px 13px -4px rgb(0 0 0 / 0.1)',
'0 22px 34px -6px rgb(0 0 0 / 0.14), 0 8px 16px -5px rgb(0 0 0 / 0.11)',
'0 26px 40px -7px rgb(0 0 0 / 0.15), 0 10px 19px -5px rgb(0 0 0 / 0.12)',
'0 31px 47px -8px rgb(0 0 0 / 0.16), 0 12px 23px -6px rgb(0 0 0 / 0.13)',
'0 36px 54px -9px rgb(0 0 0 / 0.17), 0 14px 27px -7px rgb(0 0 0 / 0.14)',
'0 42px 62px -10px rgb(0 0 0 / 0.18), 0 16px 31px -8px rgb(0 0 0 / 0.15)',
'0 48px 70px -11px rgb(0 0 0 / 0.2), 0 18px 36px -9px rgb(0 0 0 / 0.16)',
'0 54px 78px -12px rgb(0 0 0 / 0.21), 0 20px 41px -10px rgb(0 0 0 / 0.17)',
'0 60px 86px -13px rgb(0 0 0 / 0.22), 0 23px 46px -11px rgb(0 0 0 / 0.18)',
'0 66px 94px -14px rgb(0 0 0 / 0.23), 0 26px 52px -12px rgb(0 0 0 / 0.19)',
'0 72px 102px -15px rgb(0 0 0 / 0.24), 0 29px 58px -13px rgb(0 0 0 / 0.2)',
'0 58px 82px -11px rgb(0 0 0 / 0.26), 0 21px 40px -11px rgb(0 0 0 / 0.22)',
],
};
const brandedComponents: ThemeOptions['components'] = {
MuiButton: {
defaultProps: {
disableElevation: true,
},
styleOverrides: {
root: ({ theme }) => ({
minWidth: 'unset',
textTransform: 'capitalize',
fontSize: '1rem',
'&:hover': {
textDecoration: 'underline',
},
[theme.breakpoints.up('md')]: {
fontSize: '0.875rem',
},
}),
},
},
};
const brandedTheme = createTheme({
...brandedTokens,
components: brandedComponents,
});
/**
* Application theme
*/
const appTheme = createTheme({
...brandedTokens,
palette: {
...brandedTokens.palette,
primary: {
main: '#1976d2',
},
},
components: {
...brandedComponents,
MuiButton: {
styleOverrides: {
root: [
brandedComponents?.MuiButton?.styleOverrides?.root,
{
transition: 'transform 0.2s ease-in-out',
'&:hover': {
transform: 'translateY(-2px)',
},
},
],
},
},
},
});
function App1() {
return (
<ThemeProvider theme={appTheme}>
<Button>App Button</Button>
</ThemeProvider>
);
}
const appTheme2 = createTheme({
...brandedTokens,
palette: {
...brandedTokens.palette,
primary: {
main: '#ffa726',
},
},
components: {
...brandedComponents,
MuiButton: {
defaultProps: {
...brandedComponents?.MuiButton?.defaultProps,
variant: 'outlined',
},
styleOverrides: {
root: [
brandedComponents?.MuiButton?.styleOverrides?.root,
({ theme }) => ({
color: theme.palette.primary.dark,
}),
],
},
},
},
});
function App2() {
return (
<ThemeProvider theme={appTheme2}>
<Button>App 2 Button</Button>
</ThemeProvider>
);
}
export default function ExtensibleThemes() {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<ThemeProvider theme={brandedTheme}>
<Button>Branded Button</Button>
</ThemeProvider>
<App1 />
<App2 />
</Box>
);
}
더 알아보기 (Learn more)
- 테마 컴포넌트 커스터마이징 — 컴포넌트 variants와 상태 스타일
- createTheme API — 테마 생성 관련 문서