|
| 1 | +--- |
| 2 | +title: Theming engine basics |
| 3 | +category: Contributor Guides/theming |
| 4 | +order: 1 |
| 5 | +--- |
| 6 | + |
| 7 | +## InstUI's theming engine |
| 8 | + |
| 9 | +InstUI's theming engine allows each UI component can to be used in isolation and support multiple themes, including dynamic themes provided at runtime, while still working within a system of components that use a shared global theme. It uses the [Emotion design library's](https://emotion.sh/) under the hood to convert JavaScript objects to CSS. |
| 10 | + |
| 11 | +### Motivation |
| 12 | + |
| 13 | +1. Two-tiered theme variable system: system-wide variables + component level variables. With this variable system, components can be themed, tested, and rendered in isolation from the rest of the system, and we can mitigate issues that may arise with system-wide theme updates. |
| 14 | + |
| 15 | +2. Runtime theme application and definition: to apply user/account level themes without using the CSS cascade. |
| 16 | + |
| 17 | +3. Prevent CSS Cascade bugs: All components should specify variants via props or component level theme variables only (no className or style overrides) with a clear API and should not rely on any external styles. |
| 18 | + |
| 19 | +4. Theme variables should be accessible in JS. |
| 20 | + |
| 21 | +5. All component styles should be scoped to the component. |
| 22 | + |
| 23 | +6. Pre-render/server-side render support (inline critical CSS). |
| 24 | + |
| 25 | +7. Use a popular, well maintained and broadly adopted JS design and theming library that supports runtime theme switching ([Emotion](https://emotion.sh/)). |
| 26 | + |
| 27 | +### InstUISettingsProvider |
| 28 | + |
| 29 | +`InstUISettingsProvider` is a React component, which wraps Emotion's own `ThemeProvider`. |
| 30 | + |
| 31 | +It accepts a `theme` prop, which should be an Instructure UI theme. |
| 32 | + |
| 33 | +It can be used in two ways. On the top level, you can provide the theme for the whole application or nested anywhere inside it. You can also provide an object with theme or component theme overrides. |
| 34 | + |
| 35 | +**For detailed usage info and examples, see the [InstUISettingsProvider](#InstUISettingsProvider) documentation page.** |
| 36 | + |
| 37 | +```jsx |
| 38 | +--- |
| 39 | +type: code |
| 40 | +--- |
| 41 | +import Button from './Button' |
| 42 | +import { InstUISettingsProvider } from '@instructure/emotion' |
| 43 | +import { canvasHighContrast } from '@instructure/ui-themes' |
| 44 | + |
| 45 | +const RenderApp = () => { |
| 46 | + return ( |
| 47 | + <InstUISettingsProvider theme={canvasHighContrast}> |
| 48 | + <Button /> |
| 49 | + </InstUISettingsProvider> |
| 50 | + ) |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +### Theme overrides |
| 55 | + |
| 56 | +A themeable component’s theme can be configured by wrapping it in an [InstUISettingsProvider](#InstUISettingsProvider) component, and/or set explicitly via its `themeOverride` prop. |
| 57 | + |
| 58 | +#### themeOverride prop |
| 59 | + |
| 60 | +The themeable components accept a `themeOverride` prop which lets you override it's component theme object. It accepts an override object or a function, which has the current `componentTheme` as its parameter. |
| 61 | + |
| 62 | +**See more on the [withStyle](#withStyle/#applying-themes) and [Using theme overrides](/#using-theme-overrides) doc pages for more info.** |
| 63 | + |
| 64 | +```js |
| 65 | +--- |
| 66 | +type: example |
| 67 | +--- |
| 68 | +<div> |
| 69 | + <Button color='primary' themeOverride={{ primaryBackground: "purple" }}> |
| 70 | + Button |
| 71 | + </Button> |
| 72 | + <Button |
| 73 | + color='primary' |
| 74 | + margin="0 0 0 small" |
| 75 | + themeOverride={(componentTheme) => ({ |
| 76 | + primaryBackground: componentTheme.successBackground, |
| 77 | + primaryBorderColor: componentTheme.successBorderColor |
| 78 | + })} |
| 79 | + > |
| 80 | + Button |
| 81 | + </Button> |
| 82 | + <Button |
| 83 | + color='primary' |
| 84 | + margin="0 0 0 small" |
| 85 | + themeOverride={(_componentTheme, currentTheme) => ({ |
| 86 | + primaryBackground: currentTheme.colors.primitives.orange57, |
| 87 | + primaryBorderColor: '#00AAA4', |
| 88 | + borderWidth: currentTheme.borders.widthLarge, |
| 89 | + borderStyle: 'dashed' |
| 90 | + })} |
| 91 | + > |
| 92 | + Button |
| 93 | + </Button> |
| 94 | +</div> |
| 95 | +``` |
| 96 | + |
| 97 | +### Global styles |
| 98 | + |
| 99 | +Write your global styles in the `styles.js` file on a "globalStyles" key. You don't have to add labels to global styles. |
| 100 | + |
| 101 | +```js |
| 102 | +--- |
| 103 | +type: code |
| 104 | +--- |
| 105 | +// styles.js |
| 106 | + |
| 107 | +return { |
| 108 | + globalStyles: { |
| 109 | + '.CodeMirror': { |
| 110 | + height: 'auto', |
| 111 | + background: componentTheme.background |
| 112 | + // ... |
| 113 | + } |
| 114 | + } |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +In the `index.js`, import `Global` from `@instructure/emotion`, which is equivalent to the [Global](https://emotion.sh/docs/globals) component of Emotion.js. |
| 119 | + |
| 120 | +In the render method, use the `<Global>` component and pass the the "globalStyles" as its `styles={}` property. |
| 121 | + |
| 122 | +```jsx |
| 123 | +--- |
| 124 | +type: code |
| 125 | +--- |
| 126 | +// index.js |
| 127 | + |
| 128 | +import { withStyle, Global } from '@instructure/emotion' |
| 129 | + |
| 130 | +// ... |
| 131 | + |
| 132 | +render() { |
| 133 | + const { styles } = this.props |
| 134 | + |
| 135 | + return ( |
| 136 | + <div css={styles.codeEditor}> |
| 137 | + <Global styles={styles.globalStyles} /> |
| 138 | + // ... |
| 139 | + </div> |
| 140 | + ) |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +### Keyframes |
| 145 | + |
| 146 | +Animations are handled with Emotion's [keyframes](https://emotion.sh/docs/keyframes) helper. |
| 147 | + |
| 148 | +Import `keyframes` from `@instructure/emotion` in the `styles.js` file. |
| 149 | + |
| 150 | +Define the animation on the top of the page as a `const` and use it in your style object where needed. **Make sure that it is defined outside of the `generateStyle` method, otherwise it is causing problems with style recalculation.** |
| 151 | + |
| 152 | +```js |
| 153 | +--- |
| 154 | +type: code |
| 155 | +--- |
| 156 | +// styles.js |
| 157 | + |
| 158 | +import { keyframes } from '@instructure/emotion' |
| 159 | + |
| 160 | +const pulseAnimation = keyframes` |
| 161 | + to { |
| 162 | + transform: scale(1); |
| 163 | + opacity: 0.9; |
| 164 | + }` |
| 165 | + |
| 166 | +const generateStyle = (componentTheme, props, state) => { |
| 167 | + // ... |
| 168 | + |
| 169 | + return { |
| 170 | + componentClass: { |
| 171 | + // ... |
| 172 | + animationName: pulseAnimation |
| 173 | + } |
| 174 | + } |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +### Writing theme tests |
| 179 | + |
| 180 | +For components with theme tests, you can use `generateComponentTheme` from `theme.js` to get the theme variables. |
| 181 | + |
| 182 | +Import the themes needed for your test, and pass them to the generator. |
| 183 | + |
| 184 | +```js |
| 185 | +--- |
| 186 | +type: code |
| 187 | +--- |
| 188 | +import { canvas, canvasHighContrast } from '@instructure/ui-themes' |
| 189 | +import generateComponentTheme from '../theme' |
| 190 | + |
| 191 | +describe('YourComponent.theme', () => { |
| 192 | + describe('with canvas theme', () => { |
| 193 | + const variables = generateComponentTheme(canvas) |
| 194 | + |
| 195 | + describe('default', () => { |
| 196 | + it('should ensure background color and text color meet 3:1 contrast', () => { |
| 197 | + expect(contrast(variables.background, variables.color)).to.be.above(3) |
| 198 | + }) |
| 199 | + }) |
| 200 | + }) |
| 201 | + describe('with the "canvas-high-contrast" theme', () => { |
| 202 | + const variables = generateComponentTheme(canvasHighContrast) |
| 203 | + |
| 204 | + describe('default', () => { |
| 205 | + it('should ensure background color and text color meet 4.5:1 contrast', () => { |
| 206 | + expect(contrast(variables.background, variables.color)).to.be.above(4.5) |
| 207 | + }) |
| 208 | + }) |
| 209 | + }) |
| 210 | +}) |
| 211 | +``` |
0 commit comments