Dark Mode

1. Project Setup

typescript 기반으로 프로젝트를 세팅하고 styled-component 를 설치합니다.

$ npx create-react-app theme-exam --template typescript
$ cd theme-exam 
$ npm i -S styled-components

2. Theme 스타일 추가

Dark, Light 두 가지 테마의 스타일을 추가합니다.

// src/interfaces/theme.ts

export interface Theme {
    color: Color
}

export interface Color {
    [key: string]: string
}

Dark Style

// src/themes/dark.ts

import { Theme } from '../interfaces/theme'

const Dark: Theme = {
    color: {
        background: "#202020",
        buttonBackground: '#3a3a3a',
        buttonColor: '#fed356'
    }
}

export default Dark

Light Style

// src/themes/light.ts 

import { Theme } from '../interfaces/theme'

const light: Theme = {
    color: {
        background: "#fff",
        buttonBackground: '#fff',
        buttonColor: '#2161f2'
    }
}

export default light

3. ThemeProvider

Dark, Light 두 가지 테마의 스타일을 다루기 위해서 styled-components 의 ThemeProvider 를 이용합니다. state 에 따라 다른 theme 에 대한 스타일을 넣어줍니다.

// src/App.tsx

import React, { useState } from 'react';
import styled, { ThemeProvider } from 'styled-components'
import light from './themes/light'
import dark from './themes/dark'

const App: React.FC = () => {
  const [isDark, setIsDark] = useState<boolean>(false)

  return (
    <ThemeProvider theme={isDark ? dark : light}>
    </ThemeProvider>
  );
}

export default App;

4. Theme 적용

ThemeProvider 에 theme props 을 이용하면 모든 styled-components 를 사용하는 곳에서 theme props 를 주입받을 수 있습니다.

// src/App.tsx

import React, { useState } from 'react';
import styled, { ThemeProvider } from 'styled-components'
import GlobalStyles from './global-styles'
import light from './themes/light'
import dark from './themes/dark'

const Container = styled.div`
  width: 100%;
  height: 100vh;
  display: flex;
  flex-flow: column;
  align-items: center;
  justify-content: center;
  background-color: ${({ theme: { color: { background } } }) => background};
`

const Button = styled.button`
  margin-top: 20px;
  padding: 5px 20px;
  border-radius: 15px;
  
  ${({ theme: { color: { buttonBackground, buttonColor } } }) => `
    background-color: ${buttonBackground};
    color: ${buttonColor};
    border: 1px solid ${buttonColor}
  `};
`

const App: React.FC = () => {
  const [isDark, setIsDark] = useState<boolean>(false)

  return (
    <ThemeProvider theme={isDark ? dark : light}>
      <Container>
        <Button onClick={() => setIsDark(!isDark)}>{isDark ? 'Light' : 'Dark'}</Button>
      </Container>
    </ThemeProvider>
  );
}

export default App;

5. SVG 추가

태양과 달 svg 를 추가합니다.

//src/assets/moon.svg

<svg width="128" height="128" style="enable-background:new 0 0 128 128;">
  <path d="M105.87,14.99c-3.74-3.39-7.91-6.38-12.42-8.89c-0.87-0.49-2-0.35-2.71,0.33 c-0.71,0.68-0.83,1.73-0.29,2.53c15.63,22.93,12.29,52.52-8.11,71.97c-11.9,11.35-27.85,17.6-44.91,17.6 c-11.39,0-22.54-2.86-32.24-8.27c-0.87-0.49-2-0.36-2.71,0.33c-0.71,0.68-0.83,1.72-0.28,2.53c2.81,4.12,6.12,7.93,9.86,11.32 c12.61,11.45,29.27,17.76,46.9,17.76c18.27,0,35.34-6.7,48.09-18.86c12.53-11.94,19.31-27.71,19.09-44.4 C125.92,42.25,118.72,26.64,105.87,14.99z" style="fill:#FCC21B;"/>
</svg>
//src/assets/sun.svg

<svg width="128" height="128" style="enable-background:new 0 0 128 128;">
    <g>
        <path d="M64,30.34c-18.59,0-33.66,15.07-33.66,33.65c0,18.59,15.07,33.66,33.66,33.66 c18.59,0,33.66-15.07,33.66-33.66C97.66,45.41,82.59,30.34,64,30.34z" style="fill:#FCC21B;" />
        <path d="M56.76,24.21L56.76,24.21h14.49c0.67,0,1.29-0.33,1.68-0.88c0.38-0.54,0.47-1.25,0.24-1.88 L65.92,1.83c-0.3-0.81-1.06-1.34-1.92-1.34s-1.62,0.54-1.92,1.34l-7.25,19.63c-0.23,0.63-0.14,1.33,0.24,1.88 C55.46,23.89,56.09,24.21,56.76,24.21z" style="fill:#FCC21B;" />
        <path d="M97.26,40.99c0.38,0.39,0.91,0.6,1.44,0.6c0.12,0,0.24-0.01,0.36-0.03c0.66-0.12,1.21-0.55,1.5-1.16 l8.76-19.01c0.36-0.78,0.19-1.69-0.41-2.3c-0.61-0.61-1.53-0.77-2.31-0.42L87.6,27.44c-0.61,0.28-1.04,0.84-1.16,1.5 c-0.12,0.66,0.1,1.33,0.56,1.81L97.26,40.99z" style="fill:#FCC21B;" />
        <path d="M126.18,62.08l-19.64-7.24c-0.63-0.23-1.33-0.14-1.88,0.24c-0.55,0.38-0.87,1-0.87,1.67l0.01,14.49 c0,0.67,0.33,1.3,0.88,1.68c0.35,0.23,0.76,0.36,1.17,0.36c0.24,0,0.48-0.04,0.71-0.13l19.64-7.24c0.8-0.29,1.34-1.06,1.34-1.93 C127.52,63.14,126.99,62.38,126.18,62.08z" style="fill:#FCC21B;" />
        <path d="M100.56,87.6c-0.28-0.61-0.84-1.04-1.5-1.16c-0.66-0.11-1.34,0.1-1.8,0.57L87.01,97.26 c-0.47,0.47-0.69,1.15-0.57,1.81c0.12,0.65,0.55,1.22,1.16,1.5l19.01,8.76c0.27,0.13,0.56,0.18,0.86,0.18 c0.53,0,1.05-0.21,1.44-0.6c0.61-0.61,0.77-1.52,0.41-2.3L100.56,87.6z" style="fill:#FCC21B;" />
        <path d="M71.24,103.78L71.24,103.78l-14.49,0.01c-0.67,0-1.29,0.33-1.67,0.88 c-0.38,0.55-0.47,1.25-0.25,1.87l7.25,19.64c0.3,0.8,1.06,1.34,1.92,1.34s1.62-0.54,1.92-1.34l7.25-19.64 c0.23-0.63,0.14-1.33-0.24-1.88C72.54,104.11,71.92,103.78,71.24,103.78z" style="fill:#FCC21B;" />
        <path d="M30.74,87.01c-0.47-0.47-1.14-0.68-1.8-0.57c-0.66,0.12-1.22,0.55-1.5,1.16l-8.76,19.01 c-0.36,0.78-0.19,1.7,0.42,2.3c0.39,0.39,0.91,0.6,1.44,0.6c0.29,0,0.58-0.06,0.86-0.19l19.01-8.77c0.61-0.28,1.04-0.84,1.16-1.5 c0.12-0.66-0.1-1.33-0.57-1.8L30.74,87.01z" style="fill:#FCC21B;" />
        <path d="M22.17,73.29c0.41,0,0.82-0.13,1.17-0.37c0.55-0.38,0.88-1.01,0.88-1.68l-0.01-14.49 c0-0.67-0.33-1.29-0.88-1.68c-0.55-0.38-1.25-0.47-1.87-0.24L1.82,62.08c-0.8,0.29-1.34,1.06-1.34,1.92c0,0.85,0.53,1.62,1.34,1.92 l19.65,7.24C21.7,73.25,21.93,73.29,22.17,73.29z" style="fill:#FCC21B;" />
        <path d="M27.45,40.4c0.28,0.61,0.84,1.04,1.5,1.16c0.12,0.02,0.24,0.03,0.36,0.03c0.54,0,1.06-0.21,1.45-0.6 L41,30.74c0.47-0.48,0.68-1.15,0.56-1.81c-0.12-0.65-0.55-1.21-1.16-1.49l-19.02-8.76c-0.78-0.36-1.69-0.19-2.3,0.42 c-0.61,0.61-0.77,1.52-0.41,2.3L27.45,40.4z" style="fill:#FCC21B;" />
    </g>
</svg>
// src/App.tsx

import React, { useState } from 'react';
import styled, { ThemeProvider } from 'styled-components'
import GlobalStyles from './global-styles'
import light from './themes/light'
import dark from './themes/dark'
import { ReactComponent as Sun } from './assets/sun.svg';
import { ReactComponent as Moon } from './assets/moon.svg';

const Container = styled.div`
  width: 100%;
  height: 100vh;
  display: flex;
  flex-flow: column;
  align-items: center;
  justify-content: center;
  background-color: ${({ theme: { color: { background } } }) => background};
`

const Button = styled.button`
  margin-top: 20px;
  padding: 5px 20px;
  border-radius: 15px;
  
  ${({ theme: { color: { buttonBackground, buttonColor } } }) => `
    background-color: ${buttonBackground};
    color: ${buttonColor};
    border: 1px solid ${buttonColor}
  `};
`

const App: React.FC = () => {
  const [isDark, setIsDark] = useState<boolean>(false)

  return (
    <ThemeProvider theme={isDark ? dark : light}>
      <GlobalStyles />
      <Container>
        {isDark ? <Sun /> : <Moon />}
        <Button onClick={() => setIsDark(!isDark)}>{isDark ? 'Light' : 'Dark'}</Button>
      </Container>
    </ThemeProvider>
  );
}

export default App;

Last updated