# [React] styled-component 筆記 ###### tags: `React` `前端筆記` `style` ## 基本建立 `styled-component` `styled-component` 是 `JS-in-CSS` 的解決方案,開發者可以透過 `JavaScript` 決定元素的樣式,與 `CSS module` 的方式不太一樣。 > 特別的是 `styled-component` 可以透過「函式」讀取外部傳入的 `props`,達到 `CSS module` 透過換 className 變更樣式。(`CSS module`:`state` 換 className,`styled-component`:`state` 換 `props`) ### 怎麼建立 `styled-component`? ```javascript! import styled from 'styled-component' const Container = styled.div` display: flex; align-items: center; justify-content: center; ` ``` 以上得到的結果與使用 `CSS module` 的結果相同 ```javascript! // xxx.jsx // ... <div className={style.container}></div> // ... // xxx.css .container { display: flex; align-items: center; justify-content: center; } ``` ## `props` 接收外部的資料 1. `props` 可以讀取 `<ThemeProvider theme={theme}>` 的 `theme` 2. `props` 可以讀取元件的 props 3. `props` 可以讀取該元素的屬性 ```javascript! const Btn = styled.button` /* 讀取 button 的 disabled 屬性 */ color: ${(props) => (props.disabled ? "red" : "green")}; ` ``` ## 如何建立全局 style 變數? 一般使用 `CSS module` 時會建立 style 變數,存放重複性的設定(比方來說顏色、字體及 RWD 斷點等設定),在 `styled-component` 中則是提供 `<ThemeProvider>` 元件,讓其子層都可以讀取元件的樣式變數,達到重複使用 style 變數的效果。 ### STEP1: 建立 `theme` 設定表 ```typescript! // 使用 TypeScript 的話就要先定義表的型別 export interface DefaultTheme colors: { primary: string; secondary: string; highlightText: string; normalText: string; lightenText: string; lightenGrayishBlue: string; white: string; lightBoxBackground: string; navBorderBottom: string; black: string; controllerBackground: string; }; fontFamily: string; } export const theme: DefaultTheme = { colors: { primary: "#ff7d1a", secondary: "#ffede0", highlightText: "#1d2025", normalText: "#68707d", lightenText: "#b6bcc8", lightenGrayishBlue: "#f7f8fd", black: "#000", white: "#ffffff", lightBoxBackground: "rgba(0, 0, 0, .5)", navBorderBottom: "#dee2e6", controllerBackground: "#f1f1f1", }, fontFamily: "Kumbh Sans", }; ``` ### STEP2: 引入 `<ThemeProvider>` 透過 `<ThemeProvider>` 讓專案可以吃到設定的 style 變數: > `<ThemeProvider>` 使用 `Context`,所以其子層都可以讀取 `<ThemeProvider>` 的 `theme` ```javascript! // ... import { theme } from '...' <ThemeProvider theme={theme}> // children components... </ThemeProvider> ``` ## 怎麼設定 style 初始化? 預設的元素會有一些很醜的設置,因此設定吃初始化(也就是建立全域 CSS)可以覆蓋預設的樣式,一般使用 `CSS module` 的話就是設定專案街口的 style 檔案,在 `styled-component` 則提供 `createGlobalStyle`(建立初始化 style)及 `<GlobalStyles />`(在專案引入初始化 style 的元件),供開發者設定初始化 style: ### STEP1: 建立 style 初始化的資料表 ```javascript! // global.ts // ref. https://www.codevertiser.com/styled-components-folder-structure/ // 建立初始化 style 的方法 import { createGlobalStyle } from "styled-components"; import { DefaultTheme } from "./theme"; // 使用 TypeScript 時引入 style 變數的型別,讓初始化設定可以使用 style 變數 export default createGlobalStyle<{ theme: DefaultTheme }>` body { font-family: ${(props) => props.theme.fontFamily} } /* Box sizing rules */ *, *::before, *::after { box-sizing: border-box; } html { font-size: 100%; } body { margin: 0; padding: 0; overflow-x: hidden; min-height: 100vh; text-rendering: optimizeSpeed; font-family: ${({ theme }) => theme.fontFamily}, sans-serif; font-size: 1rem; line-height: 1; } h1, h2, h3, h4, h5, h6, p, ul, figure, blockquote, dl, dd { padding: 0; margin: 0; } button { border: none; background-color: transparent; font-family: inherit; padding: 0; cursor: pointer; } /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ ul[role="list"], ol[role="list"] { list-style: none; } li { list-style-type: none; } /* Set core root defaults */ html:focus-within { scroll-behavior: smooth; } /* A elements that don't have a class get default styles */ a:not([class]) { text-decoration-skip-ink: auto; } /* Make images easier to work with */ img, picture { max-width: 100%; display: block; } /* Inherit fonts for inputs and buttons */ input, button, textarea, select { font: inherit; } /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ @media (prefers-reduced-motion: reduce) { html:focus-within { scroll-behavior: auto; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } `; ``` ### STEP2: 透過 `<GlobalStyles />` 於專案引入初始化設定 ```javascript! // ... <ThemeProvider theme={theme}> // 1. 通常就直接放在 <ThemeProvider> 的第一層,讓專案可以吃到初始化設定 // 2. 因為放在 <ThemeProvider> 之內,所以可以透過函式讀取 theme 的變數 <GlobalStyles /> // children components... </ThemeProvider> ``` ### 官方文件範例 ```javascript! // Define our button, but with the use of props.theme this time // 建立 button,並且從外部收顏色設定 const Button = styled.button` font-size: 1em; margin: 1em; padding: 0.25em 1em; border-radius: 3px; /* Color the border and text with theme.main */ color: ${props => props.theme.main}; border: 2px solid ${props => props.theme.main}; `; // We are passing a default theme for Buttons that arent wrapped in the ThemeProvider // 建立預設的 theme Button.defaultProps = { theme: { main: "palevioletred" } } // Define what props.theme will look like // 建立 ThemeProvider 的 theme 資料表 const theme = { main: "mediumseagreen" }; render( <div> // 這裡會吃到預設的 theme <Button>Normal</Button> <ThemeProvider theme={theme}> // 這裡會吃到 ThemeProvider 的 theme 資料表 <Button>Themed</Button> </ThemeProvider> </div> ); ``` ## 怎麼建立 RWD 斷點設定? 也可以額外建立斷點設定的資料表,統一管理專案的斷點規範: ```javascript! // ref. https://www.codevertiser.com/styled-components-folder-structure/ const size = { xs: '400px', // for small screen mobile sm: '600px', // for mobile screen md: '900px', // for tablets lg: '1280px', // for laptops xl: '1440px', // for desktop / monitors xxl: '1920px', // for big screens } // 最後會取得 '(max-width: xxx px)' export const device = { xs: `(max-width: ${size.xs})`, sm: `(max-width: ${size.sm})`, md: `(max-width: ${size.md})`, lg: `(max-width: ${size.lg})`, xl: `(max-width: ${size.xl})`, xxl: `(max-width: ${size.xxl})`, } ``` 之後在元件內引入即可: ```javascript! // ref. https://www.codevertiser.com/styled-components-folder-structure/ import styled, { ThemeProvider } from 'styled-components' import { device } from './styles/BreakPoints' import { theme } from './styles/Theme' const Title = styled.h1` color: ${(props) => props.theme.colors.primaryTextColor}; font-size: 48px; @media ${device.md} { font-size: 32px; } ` function App() { return ( <ThemeProvider theme={theme}> <Title>Hello world</Title> </ThemeProvider> ) } export default App ``` ## 延展 style ### 1. 繼承元件設定 `styled(元件)` ```javascript! // 建立基本的 btn const BaseBtn = styled.button` color: red; border: 1px solid black; ` // 透過 `styled(元件)` 可以直接繼承該元件的 style 並重新包裝新的設定 const FancyBtn = styled(BaseBtn)` background-color: green; ``` ### 2. 直接拿元件,可以於內層元件內撰寫外層階級設定 打破一般 CSS 需要在外層定義內層的語法,直接在內層抓外層元素變更樣式: ```javascript! // ref. https://styled-components.com/docs/advanced#referring-to-other-components // 定義外層 a const Link = styled.a` display: flex; align-items: center; padding: 5px 10px; background: papayawhip; color: palevioletred; `; // 定義 svg const Icon = styled.svg` flex: none; transition: fill 0.25s; width: 48px; height: 48px; /* 強大的設定,可以直接於元件 style 設定中抓其他元件,並且不像一般 CSS 需要從外層 -> 內層定義,可以直接在內層定義外層設定 */ /* 等價於 a { &:hover { fill: rebeccapurple } } */ ${Link}:hover & { fill: rebeccapurple; } `; const Label = styled.span` display: flex; align-items: center; line-height: 1.2; &::before { content: '◀'; margin: 0 10px; } `; // 渲染的階級是 a > svg | span render( <Link href="#"> <Icon viewBox="0 0 20 20"> <path d="M10 15h8c1 0 2-1 2-2V3c0-1-1-2-2-2H2C1 1 0 2 0 3v10c0 1 1 2 2 2h4v4l4-4zM5 7h2v2H5V7zm4 0h2v2H9V7zm4 0h2v2h-2V7z"/> </Icon> <Label>Hovering my parent changes my style!</Label> </Link> ); ``` ![](https://hackmd.io/_uploads/SyWpZOoQn.gif) (可以發現 `a:hover` 時 svg 會改變顏色) ### 3. 也可以像是 `SCSS` 使用 `@extend` 定義重複性的屬性設置 使用 `SCSS` 時可以定義一個 class 處理重複性的屬性: ```sass! .test { display: flex; align-items: center; justify-content: center; } .target { // .test 的屬性會被灌入 .target 的設定中 @extend test; color: red; } ``` `styled-component` 也提供 `css` 的方法定義重複性的樣式設定: ```typescript! import styled, { css } from "styled-components"; // 這個方法可以建立原子性的 style -> 建立成一個常數,之後直接在不同的 styled-component 內使用 // 也可以透過定義型別,為 styled-component 增加彈性 const TestCss = css<{ backgroundColor?: string }>` padding: 24px; width: 40vw; background-color: ${({ backgroundColor }) => backgroundColor as string | "auto"}; `; export const Container = styled.div` background-color: ${({ theme }) => theme.colors.white}; // 引用的方式 ${TestCss} // padding: 24px; // width: 40vw; `; ``` ## 該如何透過 `props` 設定選擇性的屬性? `styled-component` 會自動忽略 `falsy value`,所以可以透過這個特定建立選擇性的屬性: ```javascript! // ref. https://styled-components.com/docs/advanced#tagged-template-literals const Title = styled.h1` /* Text centering won't break if props.upsidedown is falsy */ /* proos.upsidedown 不是 falsy value 時才會有 transform: rotate(180deg); 的屬性 */ ${props => props.upsidedown && 'transform: rotate(180deg);'} text-align: center; `; ``` ## Animation `styled-component` 也有 `keyframse` 的方法建立動畫: ```javascript! import styled, { keyframes } from "styled-components"; export const fadeIn = keyframes` 0% { opacity: 0; bottom: 0; } 100% { opacity: 1; bottom: 5%; } `; export const Container = styled.div` animation: ${fadeIn} 0.5s linear; `; ``` ## 基本的專案結構設置 ```shell! src ├── assets --> 存放圖片、字體 | ├── fonts | ├── icons | └── images ├── components --> 元件 └── styles --> style 設定 ├── breakpoints.ts -> 設定斷點 ├── font.module.css -> 文字檔案 ├── global.ts -> 初始化 CSS(全域 CSS) └── theme.ts -> style 變數 ``` ## Recap 1. `styled-component` 可以透過 `props` 讀取 **元素屬性**、**元件 `props`**及**`<ThemeProvider theme>`** 2. 使用 TypeScript 時可以定義 styled component 的型別 3. 使用 `styled(component)`、直接在 styled component 抓其他 styled component 直接寫巢狀(與一般 `CSS` 需要從外到內定義的方式不同)及使用 `CSS` 定義重複的屬性 4. `createGlobalStyle` 定義全域(初始化)CSS 設定 ## 參考資料 1. [React Styled Components Folder Structure Best Practices for Scalable Application](https://www.codevertiser.com/styled-components-folder-structure/) 2. [Documentation](https://styled-components.com/docs) 3. [[note] styled-component 筆記](https://pjchender.dev/npm/npm-styled-components/)