UNPKG

15.2 kBMarkdownView Raw
1# @uifabric/merge-styles
2
3The `merge-styles` library provides utilities for loading styles through javascript. It is designed to make it simple to style components through javascript. It generates css classes, rather than using inline styling, to ensure we can use css features like pseudo selectors (:hover) and parent/child selectors (media queries).
4
5The library was built for speed and size; the entire package is 2.62k gzipped. It has no dependencies other than `tslib`.
6
7Simple usage:
8
9```
10import { mergeStyles, mergeStyleSets } from '@uifabric/merge-styles';
11
12// Produces 'css-0' class name which can be used anywhere
13mergeStyles({ background: 'red' });
14
15// Produces a class map for a bunch of rules all at once
16mergeStyleSets({
17 root: { background: 'red' },
18 child: { background: 'green' }
19});
20
21// Returns { root: 'root-0', child: 'child-1' }
22```
23
24Both utilities behave similar to a deep Object.assign; you can collapse many objects down into one class name or class map.
25
26The basic idea is to provide tools which can take in one or more css styling objects representing the styles for a given element, and return a single class name. If the same set of styling is passed in, the same name returns and nothing is re-registered.
27
28## Motivation
29
30Defining rules at runtime has a number of benefits over traditional build time staticly produced css:
31
32- Only register classes that are needed, when they're needed, reducing the overall selector count and improving TTG.
33
34- Dynamically create new class permutations based on contextual theming requirements. (Use a different theme inside of a DIV without downloading multiple copies of the css rule definitions.)
35
36- Use JavaScript to define the class content (using utilities like color converters, or reusing constant numbers becomes possible.)
37
38- Allow control libraries to merge customized styling in with their rules, avoiding complexities like css selector specificity.
39
40- Simplify RTL processing; lefts become rights in RTL, in the actual rules. No complexity like `html[dir=rtl]` prefixes necessary, which alleviates unexpected specificity bugs. (You can use `/* noflip */` comments to avoid flipping if needed.)
41
42- Reduce bundle size. Automatically handles vendor prefixing, unit providing, RTL flipping, and margin/padding expansion (e.g. margin will automatically expand out to margin TRBL, so that we avoid specificity problems when merging things together.)
43
44- Reduce the build time overhead of running through CSS preprocessors.
45
46- TypeScript type safety; spell "background" wrong and get build breaks.
47
48## What tradeoffs are there? Are there downsides to using JavaScript to process styling?
49
50In static solutions, there is very little runtime evaluation required; everything is injected as-is. Things like auto prefixing and language specific processing like sass mixins are all evaluated at build time.
51
52In runtime styling, much of this is evaluated in the browser, so you are paying a cost in doing this. However, with performance optimizations like memoization, you can minimize this quite a bit, and you gain all of the robustness enumerated above.
53
54# API
55
56The api surfaces consists of 3 methods and a handful of interfaces:
57
58`mergeStyles(..args[]: IStyle[]): string` - Takes in one or more style objects, merges them in the right order, and produces a single css class name which can be injected into any component.
59
60`mergeStyleSets(...args[]: IStyleSet[]): { [key: string]: string }` - Takes in one or more style set objects, each consisting of a set of areas, each which will produce a class name. Using this is analogous to calling mergeStyles for each property in the object, but ensures we maintain the set ordering when multiple style sets are merged.
61
62`concatStyleSets(...args[]: IStyleSet[]): IStyleSet` - In some cases you simply need to combine style sets, without actually generating class names (it is costs in performance to generate class names.) This tool returns a single set merging many together.
63
64`concatStyleSetsWithProps(props: {}, ...args[]: IStyleSet[]): IStyleSet` - Similar to `concatStyleSet` except that style sets which contain functional evaluation of styles are evaluated prior to concatenating.
65
66Example:
67
68```tsx
69const result = concatStyleSetsWithProps<IFooProps, IFooStyles>(
70 { foo: 'bar' },
71 (props: IFooProps) => ({ root: { background: props.foo } }),
72 (props: IFooProps) => ({ root: { color: props.foo } }),
73);
74```
75
76## Vocabulary
77
78A **style object** represents the collection of css rules, except that the names are camelCased rather than kebab-cased. Example:
79
80```tsx
81let style = {
82 backgroundColor: 'red',
83 left: 42,
84};
85```
86
87Additionally, **style objects** can contain selectors:
88
89```tsx
90let style = {
91 backgroundColor: 'red',
92 ':hover': {
93 backgroundColor: 'blue';
94 },
95 '.parent &': { /* parent selector */ },
96 '& .child': { /* child selector */ }
97};
98```
99
100A **style set** represents a map of area to style object. When building a component, you need to generate a class name for each element that requires styling. You would define this in a **style set**.
101
102```tsx
103let styleSet = {
104 root: { background: 'red' },
105 button: { margin: 42 },
106};
107```
108
109## Basic usage
110
111When building a component, you will need a **style set** map of class names to inject into your elements' class attributes.
112
113The recommended pattern is to provide the classnames in a separate function, typically in a separate file `ComponentName.classNames.ts`.
114
115```tsx
116import { IStyle, mergeStyleSets } from '@uifabric/merge-styles';
117
118export interface IComponentClassNames {
119 root: string;
120 button: string;
121 buttonIcon: string;
122}
123
124export const getClassNames = (): IComponentClassNames => {
125 return mergeStyleSets({
126 root: {
127 background: 'red',
128 },
129
130 button: {
131 backgroundColor: 'green',
132 },
133
134 buttonIcon: {
135 margin: 10,
136 },
137 });
138};
139```
140
141The class map can then be used in a component:
142
143```tsx
144import { getClassNames } from './MyComponent.classNames';
145
146export const MyComponent = () => {
147 let { root, button, buttonIcon } = getClassNames();
148
149 return (
150 <div className={root}>
151 <button className={button}>
152 <i className={buttonIcon} />
153 </button>
154 </div>
155 );
156};
157```
158
159## Selectors
160
161### Basic pseudo-selectors (:hover, :active, etc)
162
163Custom selectors can be defined within `IStyle` definitions:
164
165```tsx
166{
167 background: 'red',
168 ':hover': {
169 background: 'green'
170 }
171}
172```
173
174By default, the rule will be appended to the current selector scope. That is, in the above scenario, there will be 2 rules inserted when using `mergeStyles`:
175
176```css
177.css-0 {
178 background: red;
179}
180.css-0:hover {
181 background: green;
182}
183```
184
185### Parent/child selectors
186
187In some cases, you may need to use parent or child selectors. To do so, you can define a selector from scratch and use the `&` character to represent the generated class name. When using the `&`, the current scope is ignored. Example:
188
189```tsx
190{
191 // selector relative to parent
192 '.ms-Fabric--isFocusVisible &': {
193 background: 'red'
194 }
195
196 // selector for child
197 '& .child' {
198 background: 'green'
199 }
200}
201```
202
203This would register the rules:
204
205```css
206.ms-Fabric--isFocusVisible .css-0 {
207 background: red;
208}
209.css-0 .child {
210 background: green;
211}
212```
213
214### Global selectors
215
216While we suggest avoiding global selectors, there are some cases which make sense to register things globally. Keep in mind that global selectors can't be guaranteed unique and may suffer from specificity problems and versioning issues in the case that two different versions of your library get rendered on the page.
217
218To register a selector globally, wrap it in a `:global()` wrapper:
219
220```tsx
221{
222 ':global(button)': {
223 overflow: 'visible'
224 }
225}
226```
227
228### Media and feature queries
229
230Media queries can be applied via selectors. For example, this style will produce a class which has a red background when above 600px, and green when at or below 600px:
231
232```tsx
233mergeStyles({
234 background: 'red',
235 '@media(max-width: 600px)': {
236 background: 'green',
237 },
238 '@supports(display: grid)': {
239 display: 'grid',
240 },
241});
242```
243
244Produces:
245
246```css
247.css-0 {
248 background: red;
249}
250
251@media (max-width: 600px) {
252 .css-0 {
253 background: green;
254 }
255}
256
257@supports (display: grid) {
258 .css-0 {
259 display: grid;
260 }
261}
262```
263
264### Referencing child elements within the mergeStyleSets scope
265
266One important concept about `mergeStyleSets` is that it produces a map of class names for the given elements:
267
268```tsx
269mergeStyleSets({
270 root: { background: 'red' }
271 thumb: { background: 'green' }
272});
273```
274
275Produces:
276
277```css
278.root-0 {
279 background: red;
280}
281.thumb-1 {
282 background: green;
283}
284```
285
286In some cases, you may need to alter a child area by interacting with the parent. For example, when the parent is hovered, change the child background. We recommend using global, non-changing static classnames
287to target the parent elements:
288
289```tsx
290const classNames = {
291 root: 'Foo-root',
292 child: 'Foo-child',
293};
294
295mergeStyleSets({
296 root: [classNames.root, { background: 'lightgreen' }],
297
298 child: [
299 classNames.child,
300 {
301 [`.${classNames.root}:hover &`]: {
302 background: 'green',
303 },
304 },
305 ],
306});
307```
308
309The important part here is that the selector does not have any mutable information. In the example above,
310if `classNames.root` were dynamic, it would require the rule to be re-registered when it mutates, which
311would be a performance hit.
312
313## Custom class names
314
315By default when using `mergeStyles`, class names that are generated will use the prefix `css-` followed by a number, creating unique rules where needed. For example, the first class name produced will be 'css-0'.
316
317When using `mergeStyleSets`, class names automatically use the area name as the prefix.
318
319Merging rules like:
320
321```ts
322mergeStyleSets({ a: { ... }, b: { ... } })
323```
324
325Will produce the class name map:
326
327```ts
328{ a: 'a-0', b: 'b-1' }
329```
330
331If you'd like to override the default prefix in either case, you can pass in a `displayName` to resolve this:
332
333```tsx
334{
335 displayName: 'MyComponent',
336 background: 'red'
337}
338```
339
340This generates:
341
342```css
343.MyComponent-0 {
344 background: red;
345}
346```
347
348## Managing conditionals and states
349
350Style objects can be represented by a simple object, but also can be an array of the objects. The merge functions will handle arrays and merge things together in the given order. They will also ignore falsey values, allowing you to conditionalize the results.
351
352In the following example, the root class generated will be different depending on the `isToggled` state:
353
354```tsx
355export const getClassNames = (isToggled: boolean): IComponentClassNames => {
356 return mergeStyleSets({
357 root: [
358 {
359 background: 'red',
360 },
361 isToggled && {
362 background: 'green',
363 },
364 ],
365 });
366};
367```
368
369## RTL support
370
371By default, nearly all of the major rtl-sensitive CSS properties will be auto flipped when the dir="rtl" flag is present on the `HTML` tag of the page.
372
373There are some rare scenarios (linear-gradients, etc) which are not flipped, for the sake of keeping the bundle size to a minimum. If there are missing edge cases, please submit a PR to address.
374
375In rare condition where you want to avoid auto flipping, you can annotate the rule with the `@noflip` directive:
376
377```tsx
378mergeStyles({
379 left: '42px @noflip',
380});
381```
382
383## Optimizing for performance
384
385Resolving the class names on every render can be an unwanted expense especially in hot spots where things are rendered frequently. To optimize, we recommend 2 guidelines:
386
3871. For your `getClassNames` function, flatten all input parameters into simple immutable values. This helps the `memoizeFunction` utility to cache the results based on the input.
388
3892. Use the `memoizeFunction` function from the `@uifabric/utilities` package to cache the results, given a unique combination of inputs. Example:
390
391```tsx
392import { memoizeFunction } from '@uifabric/utilities';
393
394export const getClassNames = memoizeFunction((isToggled: boolean) => {
395 return mergeStyleSets({
396 // ...
397 });
398});
399```
400
401## Registering fonts
402
403Registering font faces example:
404
405```tsx
406import { fontFace } from '@uifabric/merge-styles';
407
408fontFace({
409 fontFamily: `"Segoe UI"`,
410 src: `url("//cdn.com/fontface.woff2) format(woff2)`,
411 fontWeight: 'normal',
412});
413```
414
415Note that in cases like `fontFamily` you may need to embed quotes in the string as shown above.
416
417## Registering keyframes
418
419Registering animation keyframes example:
420
421```tsx
422import { keyframes, mergeStyleSets } from '@uifabric/merge-styles';
423
424let fadeIn = keyframes({
425 from: {
426 opacity: 0,
427 },
428 to: {
429 opacity: 1,
430 },
431});
432
433export const getClassNames = () => {
434 return mergeStyleSets({
435 root: {
436 animationName: fadeIn,
437 },
438 });
439};
440```
441
442## Controlling where styles are injected
443
444By default `merge-styles` will initially inject a `style` element into the document head as the first node and then append and new `style` elements as next sibling to the previous one added.
445
446In some cases you may want to control where styles are injected to ensure some stylesheets are more specific than others. To do this, you can add a placeholder `style` element in the head with `data-merge-styles` attribute:
447
448```html
449<head>
450 <style data-merge-styles></style>
451</head>
452```
453
454Merge styles will ensure that any generated styles are added after the placeholder.
455
456## Server-side rendering
457
458You can import `renderStatic` method from the `/lib/server` entry to render content and extract the css rules that would have been registered, as a string.
459
460Example:
461
462```tsx
463import { renderStatic } from '@uifabric/merge-styles/lib/server';
464
465let { html, css } = renderStatic(() => {
466 return ReactDOM.renderToString(...);
467});
468```
469
470Caveats for server-side rendering:
471
472- Rules registered in the file scope of code won't be re-evaluated and therefore won't be included in the result. Try to avoid using classes which are not evaluated at runtime.
473
474For example:
475
476```tsx
477const rootClass = mergeStyles({ background: 'red' });
478const App = () => <div className={rootClass} />;
479
480// App will render, but "rootClass" is a string which won't get re-evaluated in this call.
481renderStatic(() => ReactDOM.renderToString(<App/>);
482```
483
484- Using `memoizeFunction` around rule calculation can help with excessive rule recalc performance overhead.
485
486- Rehydration on the client may result in mismatched rules. You can apply a namespace on the server side to ensure there aren't name collisions.
487
488## Working with content security policy (CSP)
489
490Some content security policies prevent style injection without a nonce. To set the nonce used by `merge-styles`:
491
492```ts
493Stylesheet.getInstance().setConfig({
494 cspSettings: { nonce: 'your nonce here' },
495});
496```
497
498If you're working inside a Fluent UI React app ([formerly Office UI Fabric React](https://developer.microsoft.com/en-us/office/blogs/ui-fabric-is-evolving-into-fluent-ui/)), this setting can also be applied using the global `window.FabricConfig.mergeStyles.cspSettings`. Note that this must be set before any Fluent UI React code is loaded, or it may not be applied properly.
499
500```ts
501window.FabricConfig = {
502 mergeStyles: {
503 cspSettings: { nonce: 'your nonce here' },
504 },
505};
506```