Color

Color (색상)

색상을 통해 의미를 전달하는 방법을 다루는 문서입니다. Material Design 가이드라인의 모든 색상이 기본 제공되며, 브랜드나 스타일을 반영하는 컬러 테마를 만들 수 있어요.

출처: 문서

본문

색상을 통해 의미를 전달해 보세요. 기본 제공되는 Material Design 가이드라인의 모든 색상에 접근할 수 있습니다.

Material Design의 color system을 사용하면 브랜드나 스타일을 반영하는 컬러 테마를 만들 수 있어요.

색상 고르기 (Picking colors)

공식 컬러 도구 (Official color tool)

Material Design 팀은 훌륭한 팔레트 구성 도구도 만들었습니다: material.io/resources/color/. 이 도구는 UI의 컬러 팔레트를 만들고, 어떤 색상 조합의 접근성 수준을 측정하는 데도 도움이 돼요.

Official color tool

출력은 createTheme() 함수에 그대로 넣을 수 있습니다.

import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    primary: {
      light: '#757ce8',
      main: '#3f50b5',
      dark: '#002884',
      contrastText: '#fff',
    },
    secondary: {
      light: '#ff7961',
      main: '#f44336',
      dark: '#ba000d',
      contrastText: '#000',
    },
  },
});

플레이그라운드 (Playground)

Material UI 문서에서 material.io/design/color 색상 체계를 테스트해 보려면, 아래의 팔레트와 슬라이더를 사용해 색상을 선택하면 됩니다. 또는 Primary와 Secondary 텍스트 필드에 hex 값을 직접 입력할 수도 있어요.

import * as React from 'react';
import PropTypes from 'prop-types';
import { rgbToHex, useTheme } from '@mui/material/styles';
import * as colors from '@mui/material/colors';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Input from '@mui/material/Input';
import Radio from '@mui/material/Radio';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import CheckIcon from '@mui/icons-material/Check';
import Slider from '@mui/material/Slider';
import { capitalize } from '@mui/material/utils';
import { resetDocsColor, setDocsColors } from '@mui/internal-core-docs/branding';
import ColorDemo from './ColorDemo';

const defaults = {
  primary: '#2196f3',
  secondary: '#f50057',
};
const hues = [
  'red',
  'pink',
  'purple',
  'deepPurple',
  'indigo',
  'blue',
  'lightBlue',
  'cyan',
  'teal',
  'green',
  'lightGreen',
  'lime',
  'yellow',
  'amber',
  'orange',
  'deepOrange',
];

const shades = [
  900,
  800,
  700,
  600,
  500,
  400,
  300,
  200,
  100,
  50,
  'A700',
  'A400',
  'A200',
  'A100',
];

const TooltipRadio = React.forwardRef(function TooltipRadio(props, ref) {
  const {
    'aria-labelledby': ariaLabelledBy,
    'aria-label': ariaLabel,
    inputProps,
    ...other
  } = props;

  return (
    <Radio
      ref={ref}
      {...other}
      inputProps={{
        ...inputProps,
        'aria-labelledby': ariaLabelledBy,
        'aria-label': ariaLabel,
      }}
    />
  );
});

TooltipRadio.propTypes = {
  // possibly opaque identifier
  'aria-label': PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  'aria-labelledby': PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  inputProps: PropTypes.object,
};

function ColorTool() {
  const theme = useTheme();
  const [state, setState] = React.useState({
    primary: defaults.primary,
    secondary: defaults.secondary,
    primaryInput: defaults.primary,
    secondaryInput: defaults.secondary,
    primaryHue: 'blue',
    secondaryHue: 'pink',
    primaryShade: 4,
    secondaryShade: 11,
  });

  const handleChangeColor = (name) => (event) => {
    const isRgb = (string) =>
      /rgb\([0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}\)/i.test(string);

    const isHex = (string) => /^#?([0-9a-f]{3})$|^#?([0-9a-f]){6}$/i.test(string);

    let {
      target: { value: color },
    } = event;

    setState((prevState) => ({
      ...prevState,
      [`${name}Input`]: color,
    }));

    let isValidColor = false;

    if (isRgb(color)) {
      isValidColor = true;
    } else if (isHex(color)) {
      isValidColor = true;
      if (!color.includes('#')) {
        color = `#${color}`;
      }
    }

    if (isValidColor) {
      setState((prevState) => ({
        ...prevState,
        [name]: color,
      }));
    }
  };

  const handleChangeHue = (name) => (event) => {
    const hue = event.target.value;
    const color = colors[hue][shades[state[`${name}Shade`]]];

    setState({
      ...state,
      [`${name}Hue`]: hue,
      [name]: color,
      [`${name}Input`]: color,
    });
  };

  const handleChangeShade = (name) => (event, shade) => {
    const color = colors[state[`${name}Hue`]][shades[shade]];
    setState({
      ...state,
      [`${name}Shade`]: shade,
      [name]: color,
      [`${name}Input`]: color,
    });
  };

  const handleChangeDocsColors = () => {
    const paletteColors = {
      primary: { ...colors[state.primaryHue], main: state.primary },
      secondary: { ...colors[state.secondaryHue], main: state.secondary },
    };

    setDocsColors(paletteColors.primary, paletteColors.secondary);

    document.cookie = `paletteColors=${JSON.stringify(
      paletteColors,
    )};path=/;max-age=31536000`;
  };

  const handleResetDocsColors = () => {
    resetDocsColor();

    document.cookie = 'paletteColors=;path=/;max-age=0';
  };

  const colorBar = (color) => {
    const background = theme.palette.augmentColor({
      color: {
        main: color,
      },
    });

    return (
      <Grid container sx={{ mt: 2 }}>
        {['dark', 'main', 'light'].map((key) => (
          <Box
            sx={{
              width: 64,
              height: 64,
              display: 'flex',
              justifyContent: 'center',
              alignItems: 'center',
            }}
            style={{ backgroundColor: background[key] }}
            key={key}
          >
            <Typography
              variant="caption"
              style={{
                color: theme.palette.getContrastText(background[key]),
              }}
            >
              {rgbToHex(background[key])}
            </Typography>
          </Box>
        ))}
      </Grid>
    );
  };

  const colorPicker = (intent) => {
    const intentInput = state[`${intent}Input`];
    const intentShade = state[`${intent}Shade`];
    const color = state[`${intent}`];

    return (
      <Grid
        size={{
          xs: 12,
          sm: 6,
          md: 4,
        }}
      >
        <Typography component="label" gutterBottom htmlFor={intent} variant="h6">
          {capitalize(intent)}
        </Typography>
        <Input
          id={intent}
          value={intentInput}
          onChange={handleChangeColor(intent)}
          fullWidth
        />
        <Box sx={{ display: 'flex', alignItems: 'center', mt: 2, mb: 2 }}>
          <Typography id={`${intent}ShadeSliderLabel`}>Shade:</Typography>
          <Slider
            sx={{ width: 'calc(100% - 80px)', ml: 3, mr: 3 }}
            value={intentShade}
            min={0}
            max={13}
            step={1}
            onChange={handleChangeShade(intent)}
            aria-labelledby={`${intent}ShadeSliderLabel`}
          />
          <Typography sx={{ minWidth: 40 }}>{shades[intentShade]}</Typography>
        </Box>
        <Box sx={{ width: 192 }}>
          {hues.map((hue) => {
            const shade =
              intent === 'primary'
                ? shades[state.primaryShade]
                : shades[state.secondaryShade];
            const backgroundColor = colors[hue][shade];

            return (
              <Tooltip placement="right" title={hue} key={hue} disableInteractive>
                <TooltipRadio
                  sx={{ p: 0 }}
                  color="default"
                  checked={state[intent] === backgroundColor}
                  onChange={handleChangeHue(intent)}
                  value={hue}
                  name={intent}
                  aria-label={hue}
                  icon={
                    <Box
                      sx={{ width: 48, height: 48 }}
                      style={{ backgroundColor }}
                    />
                  }
                  checkedIcon={
                    <Box
                      sx={{
                        width: 48,
                        height: 48,
                        border: 1,
                        borderColor: 'white',
                        color: 'common.white',
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                      }}
                      style={{ backgroundColor }}
                    >
                      <CheckIcon style={{ fontSize: 30 }} />
                    </Box>
                  }
                />
              </Tooltip>
            );
          })}
        </Box>
        {colorBar(color)}
      </Grid>
    );
  };

  return (
    <Grid container spacing={5} sx={{ p: 0 }}>
      {colorPicker('primary')}
      {colorPicker('secondary')}
      <Grid
        size={{
          xs: 12,
          sm: 6,
          md: 4,
        }}
      >
        <ColorDemo data={state} />
      </Grid>
      <Grid size={12}>
        <Button variant="contained" onClick={handleChangeDocsColors}>
          Set Docs Colors
        </Button>
        <Button variant="outlined" onClick={handleResetDocsColors} sx={{ ml: 1 }}>
          Reset Docs Colors
        </Button>
      </Grid>
    </Grid>
  );
}

export default ColorTool;

컬러 샘플에 표시된 출력은 그대로 createTheme() 함수에 붙여 넣을 수 있어요.(ThemeProvider와 함께 사용하면 됩니다.)

import { createTheme } from '@mui/material/styles';
import { purple } from '@mui/material/colors';

const theme = createTheme({
  palette: {
    primary: {
      main: purple[500],
    },
    secondary: {
      main: '#f44336',
    },
  },
});

main shade만 제공하면 됩니다(light, dark, contrastText를 추가로 커스터마이즈하고 싶지 않다면요). 다른 색상은 Theme customization 섹션에 설명된 대로 createTheme()가 계산하기 때문이에요.

기본 primary 또는 secondary shade를 사용하고 있다면, 색상 객체를 제공함으로써 createTheme()이 main, light, dark에 대해 material color에서 적절한 shade를 사용하게 됩니다.

커뮤니티 도구 (Tools by the community)

  • mui-theme-creator: Material UI 컴포넌트 라이브러리용 테마를 설계하고 커스터마이즈하는 데 도움을 주는 도구. 다양한 컴포넌트와 그것들이 테마에 어떻게 영향받는지 보여주는 기본 사이트 템플릿을 포함합니다.
  • MUI Theme Builder: Material UI 테마를 생성, 미리보기, 편집하는 도구.
  • Material palette generator: 입력한 어떤 색상에 대해서도 팔레트를 생성할 수 있는 Material 팔레트 생성기.
  • MUI Theme Customizer: Material UI 테마를 실험하고 생성하기 쉽게 해 주는 도구.

2014 Material Design 컬러 팔레트

이 컬러 팔레트들은 원래 2014년에 Material Design이 만든 것으로, 조화롭게 함께 동작하도록 설계된 색상들로 구성되어 있으며 브랜드 팔레트를 개발하는 데 사용할 수 있어요. 자신만의 조화로운 팔레트를 만들려면 팔레트 생성 도구를 사용하세요.

중요한 용어 (Important Terms)

  • Palette: 팔레트는 색상, 즉 hue와 그 shade의 모음입니다. Material UI는 Material Design 가이드라인의 모든 색상을 제공합니다. 이 컬러 팔레트는 서로 조화롭게 동작하는 색상들로 설계되었습니다.
  • Hue & Shade: 팔레트 안의 단일 색상은 "red" 같은 hue와 "500" 같은 shade로 구성됩니다. "red 50"은 red의 가장 밝은 shade(분홍! )이고, "red 900"이 가장 어둡죠. 또한 대부분의 hue에는 A 접두사가 붙은 "accent" shade가 있습니다.

컬러 팔레트 (Color palette)

HUE(red, pink 등)와 SHADE(500, 600 등)가 주어지면 다음과 같이 색상을 import할 수 있어요.

import { red } from '@mui/material/colors';

const color = red[500];
import Box from '@mui/material/Box';
import { styled, useTheme } from '@mui/material/styles';
import * as colors from '@mui/material/colors';

const mainColors = [
  'Red',
  'Pink',
  'Purple',
  'Deep Purple',
  'Indigo',
  'Blue',
  'Light Blue',
  'Cyan',
  'Teal',
  'Green',
  'Light Green',
  'Lime',
  'Yellow',
  'Amber',
  'Orange',
  'Deep Orange',
  'Brown',
  'Grey',
  'Blue Grey',
];

const mainPalette = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
const altPalette = ['A100', 'A200', 'A400', 'A700'];

const ColorGroup = styled('ul', { name: 'ColorGroup' })(({ theme }) => ({
  padding: 0,
  margin: theme.spacing(0, 2, 2, 0),
  flexGrow: 1,
  [theme.breakpoints.up('sm')]: {
    flexGrow: 0,
    width: '30%',
  },
}));

const ColorValue = styled('span', { name: 'ColorValue' })(({ theme }) => ({
  ...theme.typography.caption,
  color: 'inherit',
  fontWeight: 'inherit',
}));

const ColorBlock = styled('li', { name: 'ColorBlock' })(
  ({ theme }) => theme.typography.body2,
);

function getColorBlock(theme, colorName, colorValue, colorTitle) {
  const bgColor = colors[colorName][colorValue];
  const fgColor = theme.palette.getContrastText(bgColor);

  let blockTitle;
  if (colorTitle) {
    blockTitle = <Box sx={{ mb: '60px' }}>{colorName}</Box>;
  }

  let rowStyle = {
    backgroundColor: bgColor,
    color: fgColor,
    listStyle: 'none',
    padding: 15,
  };

  if (colorValue.toString().startsWith('A1')) {
    rowStyle = {
      ...rowStyle,
      marginTop: 4,
    };
  }

  return (
    <ColorBlock style={rowStyle} key={colorValue}>
      {blockTitle}
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
        }}
      >
        <span>{colorValue}</span>
        <ColorValue>{bgColor}</ColorValue>
      </Box>
    </ColorBlock>
  );
}

function getColorGroup(options) {
  const { theme, color, showAltPalette } = options;
  const cssColor = color
    .replace(' ', '')
    .replace(color.charAt(0), color.charAt(0).toLowerCase());
  let colorsList = [];
  colorsList = mainPalette.map((mainValue) =>
    getColorBlock(theme, cssColor, mainValue),
  );

  if (showAltPalette) {
    altPalette.forEach((altValue) => {
      colorsList.push(getColorBlock(theme, cssColor, altValue));
    });
  }

  return (
    <ColorGroup key={cssColor}>
      {getColorBlock(theme, cssColor, 500, true)}
      <Box sx={{ height: 4, listStyle: 'none' }} component="li" role="separator" />
      {colorsList}
    </ColorGroup>
  );
}

function Color() {
  const theme = useTheme();

  return (
    <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
      {mainColors.map((mainColor) =>
        getColorGroup({
          theme,
          color: mainColor,
          showAltPalette: true,
        }),
      )}
    </Box>
  );
}

export default Color;

예시 (Examples)

예를 들어 보색이 되는 primary와 accent 색상, "red 500"과 "purple A200"을 다음과 같이 참조할 수 있어요.

import { purple, red } from '@mui/material/colors';

const primary = red[500]; // #f44336
const accent = purple['A200']; // #e040fb
const accent = purple.A200; // #e040fb (alternative method)

접근성 (Accessibility)

WCAG 2.2 Success Criterion 1.4.3은 텍스트와 텍스트 이미지의 시각적 표현에 대해 최소 4.5:1의 대비 비율을 권장합니다. Material UI는 현재 3:1의 대비 비율만 적용합니다. WCAG 2.2 Level AA 준수를 충족하고 싶다면, Theme customization 섹션에 설명된 대로 최소 대비 비율을 높일 수 있어요.

더 알아보기 (Learn more)