UNPKG

19.8 kBMarkdownView Raw
1# Aphrodite [![npm version](https://badge.fury.io/js/aphrodite.svg)](https://badge.fury.io/js/aphrodite) [![Build Status](https://travis-ci.org/Khan/aphrodite.svg?branch=master)](https://travis-ci.org/Khan/aphrodite) [![Coverage Status](https://coveralls.io/repos/github/Khan/aphrodite/badge.svg?branch=master)](https://coveralls.io/github/Khan/aphrodite?branch=master) [![Gitter chat](https://img.shields.io/gitter/room/Khan/aphrodite.svg)](https://gitter.im/Khan/aphrodite) [![gzip size][gzip-badge]][unpkg-dist] [![size][size-badge]][unpkg-dist]
2
3_Framework-agnostic CSS-in-JS with support for server-side rendering, browser prefixing, and minimum CSS generation._
4
5Support for colocating your styles with your JavaScript component.
6
7- Works great with and without React
8- Supports media queries without window.matchMedia
9- Supports pseudo-selectors like `:hover`, `:active`, etc. without needing to
10 store hover or active state in components. `:visited` works just fine too.
11- Supports automatic global `@font-face` detection and insertion.
12- Respects precedence order when specifying multiple styles
13- Requires no AST transform
14- Injects only the exact styles needed for the render into the DOM.
15- Can be used for server rendering
16- Few dependencies, small (20k, 6k gzipped)
17- No external CSS file generated for inclusion
18- Autoprefixes styles
19
20# Installation
21
22Aphrodite is distributed via [npm](https://www.npmjs.com/):
23
24```
25npm install --save aphrodite
26```
27
28# API
29
30If you'd rather watch introductory videos, you can find them [here](https://www.youtube.com/playlist?list=PLo4Zh55ZzNSBP78pCD0dZJi9zf8CA72_M).
31
32```jsx
33import React, { Component } from 'react';
34import { StyleSheet, css } from 'aphrodite';
35
36class App extends Component {
37 render() {
38 return <div>
39 <span className={css(styles.red)}>
40 This is red.
41 </span>
42 <span className={css(styles.hover)}>
43 This turns red on hover.
44 </span>
45 <span className={css(styles.small)}>
46 This turns red when the browser is less than 600px width.
47 </span>
48 <span className={css(styles.red, styles.blue)}>
49 This is blue.
50 </span>
51 <span className={css(styles.blue, styles.small)}>
52 This is blue and turns red when the browser is less than
53 600px width.
54 </span>
55 </div>;
56 }
57}
58
59const styles = StyleSheet.create({
60 red: {
61 backgroundColor: 'red'
62 },
63
64 blue: {
65 backgroundColor: 'blue'
66 },
67
68 hover: {
69 ':hover': {
70 backgroundColor: 'red'
71 }
72 },
73
74 small: {
75 '@media (max-width: 600px)': {
76 backgroundColor: 'red',
77 }
78 }
79});
80```
81
82## Conditionally Applying Styles
83
84Note: If you want to conditionally use styles, that is simply accomplished via:
85
86```jsx
87const className = css(
88 shouldBeRed() ? styles.red : styles.blue,
89 shouldBeResponsive() && styles.small,
90 shouldBeHoverable() && styles.hover
91)
92
93<div className={className}>Hi</div>
94```
95
96This is possible because any falsey arguments will be ignored.
97
98## Combining Styles
99
100To combine styles, pass multiple styles or arrays of styles into `css()`. This is common when combining styles from an owner component:
101
102```jsx
103class App extends Component {
104 render() {
105 return <Marker styles={[styles.large, styles.red]} />;
106 }
107}
108
109class Marker extends Component {
110 render() {
111 // css() accepts styles, arrays of styles (including nested arrays),
112 // and falsy values including undefined.
113 return <div className={css(styles.marker, this.props.styles)} />;
114 }
115}
116
117const styles = StyleSheet.create({
118 red: {
119 backgroundColor: 'red'
120 },
121
122 large: {
123 height: 20,
124 width: 20
125 },
126
127 marker: {
128 backgroundColor: 'blue'
129 }
130});
131```
132
133## Resetting Style Cache
134
135The `reset` function can be used to reset the HTML style tag, injection buffer, and injected cache. Useful when Aphrodite needs to be torn down and set back up.
136
137```js
138import { reset } from 'aphrodite';
139
140reset();
141```
142
143While the `resetInjectedStyle` function can be used to reset the injected cache for a single key (usually the class name).
144
145```js
146import { resetInjectedStyle } from 'aphrodite';
147
148resetInjectedStyle('class_1sAs8jg');
149```
150
151## Server-side rendering
152
153To perform server-side rendering, make a call to `StyleSheetServer.renderStatic`, which takes a callback. Do your rendering inside of the callback and return the generated HTML. All of the calls to `css()` inside of the callback will be collected and the generated css as well as the generated HTML will be returned.
154
155Rehydrating lets Aphrodite know which styles have already been inserted into the page. If you don't rehydrate, Aphrodite might add duplicate styles to the page.
156
157To perform rehydration, call `StyleSheet.rehydrate` with the list of generated class names returned to you by `StyleSheetServer.renderStatic`.
158
159Note: If you are using `aphrodite/no-important` in your project and you want to render it on server side, be sure to import `StyleSheetServer` from `aphrodite/no-important` otherwise you are going to get an error.
160
161As an example:
162
163```js
164import { StyleSheetServer } from 'aphrodite';
165
166// Contains the generated html, as well as the generated css and some
167// rehydration data.
168var {html, css} = StyleSheetServer.renderStatic(() => {
169 return ReactDOMServer.renderToString(<App/>);
170});
171
172// Return the base HTML, which contains your rendered HTML as well as a
173// simple rehydration script.
174return `
175 <html>
176 <head>
177 <style data-aphrodite>${css.content}</style>
178 </head>
179 <body>
180 <div id='root'>${html}</div>
181 <script src="./bundle.js"></script>
182 <script>
183 StyleSheet.rehydrate(${JSON.stringify(css.renderedClassNames)});
184 ReactDOM.render(<App/>, document.getElementById('root'));
185 </script>
186 </body>
187 </html>
188`;
189```
190
191## Disabling `!important`
192
193By default, Aphrodite will append `!important` to style definitions. This is
194intended to make integrating with a pre-existing codebase easier. If you'd like
195to avoid this behaviour, then instead of importing `aphrodite`, import
196`aphrodite/no-important`. Otherwise, usage is the same:
197
198```js
199import { StyleSheet, css } from 'aphrodite/no-important';
200```
201
202## Minifying style names
203
204By default, Aphrodite will minify style names down to their hashes in production
205(`process.env.NODE_ENV === 'production'`). You can override this behavior by
206calling `minify` with `true` or `false` before calling `StyleSheet.create`.
207
208This is useful if you want to facilitate debugging in production for example.
209
210```js
211import { StyleSheet, minify } from 'aphrodite';
212
213// Always keep the full style names
214minify(false);
215
216// ... proceed to use StyleSheet.create etc.
217```
218
219## Font Faces
220
221Creating custom font faces is a special case. Typically you need to define a global `@font-face` rule. In the case of Aphrodite we only want to insert that rule if it's actually being referenced by a class that's in the page. We've made it so that the `fontFamily` property can accept a font-face object (either directly or inside an array). A global `@font-face` rule is then generated based on the font definition.
222
223```js
224const coolFont = {
225 fontFamily: "CoolFont",
226 fontStyle: "normal",
227 fontWeight: "normal",
228 src: "url('coolfont.woff2') format('woff2')"
229};
230
231const styles = StyleSheet.create({
232 headingText: {
233 fontFamily: coolFont,
234 fontSize: 20
235 },
236 bodyText: {
237 fontFamily: [coolFont, "sans-serif"]
238 fontSize: 12
239 }
240});
241```
242
243Aphrodite will ensure that the global `@font-face` rule for this font is only inserted once, no matter how many times it's referenced.
244
245## Animations
246
247Similar to [Font Faces](#font-faces), Aphrodite supports keyframe animations, but it's treated as a special case. Once we find an instance of the animation being referenced, a global `@keyframes` rule is created and appended to the page.
248
249Animations are provided as objects describing the animation, in typical `@keyframes` fashion. Using the `animationName` property, you can supply a single animation object, or an array of animation objects. Other animation properties like `animationDuration` can be provided as strings.
250
251```js
252const translateKeyframes = {
253 '0%': {
254 transform: 'translateX(0)',
255 },
256
257 '50%': {
258 transform: 'translateX(100px)',
259 },
260
261 '100%': {
262 transform: 'translateX(0)',
263 },
264};
265
266const opacityKeyframes = {
267 'from': {
268 opacity: 0,
269 },
270
271 'to': {
272 opacity: 1,
273 }
274};
275
276const styles = StyleSheet.create({
277 zippyHeader: {
278 animationName: [translateKeyframes, opacityKeyframes],
279 animationDuration: '3s, 1200ms',
280 animationIterationCount: 'infinite',
281 },
282});
283```
284
285Aphrodite will ensure that `@keyframes` rules are never duplicated, no matter how many times a given rule is referenced.
286
287# Use without React
288
289Aphrodite was built with React in mind but does not depend on React. Here, you can see it
290used with [Web Components][webcomponents]:
291
292```js
293import { StyleSheet, css } from 'aphrodite';
294
295const styles = StyleSheet.create({
296 red: {
297 backgroundColor: 'red'
298 }
299});
300
301class App extends HTMLElement {
302 attachedCallback() {
303 this.innerHTML = `
304 <div class="${css(styles.red)}">
305 This is red.
306 </div>
307 `;
308 }
309}
310
311document.registerElement('my-app', App);
312```
313
314# Caveats
315
316## Style injection and buffering
317
318Aphrodite will automatically attempt to create a `<style>` tag in the document's `<head>` element to put its generated styles in. Aphrodite will only generate one `<style>` tag and will add new styles to this over time. If you want to control which style tag Aphrodite uses, create a style tag yourself with the `data-aphrodite` attribute and Aphrodite will use that instead of creating one for you.
319
320To speed up injection of styles, Aphrodite will automatically try to buffer writes to this `<style>` tag so that minimum number of DOM modifications happen.
321
322Aphrodite uses [asap](https://github.com/kriskowal/asap) to schedule buffer flushing. If you measure DOM elements' dimensions in `componentDidMount` or `componentDidUpdate`, you can use `setTimeout` or `flushToStyleTag` to ensure all styles are injected.
323
324```js
325import { StyleSheet, css } from 'aphrodite';
326
327class Component extends React.Component {
328 render() {
329 return <div ref="root" className={css(styles.div)} />;
330 }
331
332 componentDidMount() {
333 // At this point styles might not be injected yet.
334 this.refs.root.offsetHeight; // 0 or 10
335
336 setTimeout(() => {
337 this.refs.root.offsetHeight; // 10
338 }, 0);
339 }
340}
341
342const styles = StyleSheet.create({
343 div: {
344 height: 10,
345 },
346});
347```
348
349## Assigning a string to a content property for a pseudo-element
350
351When assigning a string to the `content` property it requires double or single quotes in CSS.
352Therefore with Aphrodite you also have to provide the quotes within the value string for `content` to match how it will be represented in CSS.
353
354As an example:
355
356```javascript
357const styles = StyleSheet.create({
358 large: {
359 ':after': {
360 content: '"Aphrodite"',
361 },
362 },
363 },
364 small: {
365 ':before': {
366 content: "'Aphrodite'",
367 },
368 },
369 });
370```
371The generated css will be:
372
373```css
374 .large_im3wl1:after {
375 content: "Aphrodite" !important;
376 }
377
378 .small_ffd5jf:before {
379 content: 'Aphrodite' !important;
380 }
381```
382
383## Overriding styles
384
385When combining multiple aphrodite styles, you are strongly recommended to merge all of your styles into a single call to `css()`, and should not combine the generated class names that aphrodite outputs (via string concatenation, `classnames`, etc.).
386For example, if you have a base style of `foo` which you are trying to override with `bar`:
387
388### Do this:
389
390```js
391const styles = StyleSheet.create({
392 foo: {
393 color: 'red'
394 },
395
396 bar: {
397 color: 'blue'
398 }
399});
400
401// ...
402
403const className = css(styles.foo, styles.bar);
404```
405
406### Don't do this:
407
408```js
409const styles = StyleSheet.create({
410 foo: {
411 color: 'red'
412 },
413
414 bar: {
415 color: 'blue'
416 }
417});
418
419// ...
420
421const className = css(styles.foo) + " " + css(styles.bar);
422```
423
424Why does it matter? Although the second one will produce a valid class name, it cannot guarantee that the `bar` styles will override the `foo` ones.
425The way the CSS works, it is not the *class name that comes last on an element* that matters, it is specificity. When we look at the generated CSS though, we find that all of the class names have the same specificity, since they are all a single class name:
426
427```css
428.foo_im3wl1 {
429 color: red;
430}
431```
432
433```css
434.bar_hxfs3d {
435 color: blue;
436}
437```
438
439In the case where the specificity is the same, what matters is *the order that the styles appear in the stylesheet*. That is, if the generated stylesheet looks like
440
441```css
442.foo_im3wl1 {
443 color: red;
444}
445.bar_hxfs3d {
446 color: blue;
447}
448```
449
450then you will get the appropriate effect of the `bar` styles overriding the `foo` ones, but if the stylesheet looks like
451
452```css
453.bar_hxfs3d {
454 color: blue;
455}
456.foo_im3wl1 {
457 color: red;
458}
459```
460
461then we end up with the opposite effect, with `foo` overriding `bar`! The way to solve this is to pass both of the styles into aphrodite's `css()` call. Then, it will produce a single class name, like `foo_im3wl1-o_O-bar_hxfs3d`, with the correctly overridden styles, thus solving the problem:
462
463```css
464.foo_im3wl1-o_O-bar_hxfs3d {
465 color: blue;
466}
467```
468
469## Object key ordering
470
471When styles are specified in Aphrodite, the order that they appear in the
472actual stylesheet depends on the order that keys are retrieved from the
473objects. This ordering is determined by the JavaScript engine that is being
474used to render the styles. Sometimes, the order that the styles appear in the
475stylesheet matter for the semantics of the CSS. For instance, depending on the
476engine, the styles generated from
477
478```js
479const styles = StyleSheet.create({
480 ordered: {
481 margin: 0,
482 marginLeft: 15,
483 },
484});
485css(styles.ordered);
486```
487
488you might expect the following CSS to be generated:
489
490```css
491margin: 0px;
492margin-left: 15px;
493```
494
495but depending on the ordering of the keys in the style object, the CSS might
496appear as
497
498```css
499margin-left: 15px;
500margin: 0px;
501```
502
503which is semantically different, because the style which appears later will
504override the style before it.
505
506This might also manifest as a problem when server-side rendering, if the
507generated styles appear in a different order on the client and on the server.
508
509If you experience this issue where styles don't appear in the generated CSS in
510the order that they appear in your objects, there are two solutions:
511
5121. Don't use shorthand properties. For instance, in the margin example above,
513 by switching from using a shorthand property and a longhand property in the
514 same styles to using only longhand properties, the issue could be avoided.
515
516 ```js
517 const styles = StyleSheet.create({
518 ordered: {
519 marginTop: 0,
520 marginRight: 0,
521 marginBottom: 0,
522 marginLeft: 15,
523 },
524 });
525 ```
526
5272. Specify the ordering of your styles by specifying them using a
528 [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map).
529 Since `Map`s preserve their insertion order, Aphrodite is able to place your
530 styles in the correct order.
531
532 ```js
533 const styles = StyleSheet.create({
534 ordered: new Map([
535 ["margin", 0],
536 ["marginLeft", 15],
537 ]),
538 });
539 ```
540
541 Note that `Map`s are not fully supported in all browsers. It can be
542 polyfilled by using a package
543 like [es6-shim](https://www.npmjs.com/package/es6-shim).
544
545## Advanced: Extensions
546
547Extra features can be added to Aphrodite using extensions.
548
549To add extensions to Aphrodite, call `StyleSheet.extend` with the extensions
550you are adding. The result will be an object containing the usual exports of
551Aphrodite (`css`, `StyleSheet`, etc.) which will have your extensions included.
552For example:
553
554```js
555// my-aphrodite.js
556import {StyleSheet} from "aphrodite";
557
558export default StyleSheet.extend([extension1, extension2]);
559
560// styled.js
561import {StyleSheet, css} from "my-aphrodite.js";
562
563const styles = StyleSheet.create({
564 ...
565});
566```
567
568**Note**: Using extensions may cause Aphrodite's styles to not work properly.
569Plain Aphrodite, when used properly, ensures that the correct styles will
570always be applied to elements. Due to CSS specificity rules, extensions might
571allow you to generate styles that conflict with each other, causing incorrect
572styles to be shown. See the global extension below to see what could go wrong.
573
574### Creating extensions
575
576Currently, there is only one kind of extension available: selector handlers.
577These kinds of extensions let you look at the selectors that someone specifies
578and generate new selectors based on them. They are used to handle pseudo-styles
579and media queries inside of Aphrodite. See the
580[`defaultSelectorHandlers` docs](src/generate.js?L8) for information about how
581to create a selector handler function.
582
583To use your extension, create an object containing a key of the kind of
584extension that you created, and pass that into `StyleSheet.extend()`:
585
586```js
587const mySelectorHandler = ...;
588
589const myExtension = {selectorHandler: mySelectorHandler};
590
591const { StyleSheet: newStyleSheet, css: newCss } = StyleSheet.extend([myExtension]);
592```
593
594As an example, you could write an extension which generates global styles like
595
596```js
597const globalSelectorHandler = (selector, _, generateSubtreeStyles) => {
598 if (selector[0] !== "*") {
599 return null;
600 }
601
602 return generateSubtreeStyles(selector.slice(1));
603};
604
605const globalExtension = {selectorHandler: globalSelectorHandler};
606```
607
608This might cause problems when two places try to generate styles for the same
609global selector however! For example, after
610
611```js
612const styles = StyleSheet.create({
613 globals: {
614 '*div': {
615 color: 'red',
616 },
617 }
618});
619
620const styles2 = StyleSheet.create({
621 globals: {
622 '*div': {
623 color: 'blue',
624 },
625 },
626});
627
628css(styles.globals);
629css(styles2.globals);
630```
631
632It isn't determinate whether divs will be red or blue.
633
634## Minify class names
635
636Minify class names by setting the environment variable `process.env.NODE_ENV`
637to the string value `production`.
638
639# Tools
640
641- [Aphrodite output tool](https://output.jsbin.com/qoseye) - Paste what you pass to `StyleSheet.create` and see the generated CSS
642- [jest-aphrodite-react](https://github.com/dmiller9911/jest-aphrodite-react) - Utilities for testing with React and Jest.
643
644# TODO
645
646- Add JSdoc
647- Consider removing !important from everything.
648
649# Other solutions
650
651- [js-next/react-style](https://github.com/js-next/react-style)
652- [dowjones/react-inline-style](https://github.com/dowjones/react-inline-style)
653- [martinandert/react-inline](https://github.com/martinandert/react-inline)
654- [milesj/aesthetic](https://github.com/milesj/aesthetic) - a React style abstraction layer with theme support
655- [airbnb/react-with-styles](https://github.com/airbnb/react-with-styles)
656
657# License (MIT)
658
659Copyright (c) 2016 Khan Academy
660
661[webcomponents]: http://w3c.github.io/webcomponents/spec/custom
662
663[gzip-badge]: http://img.badgesize.io/https://unpkg.com/aphrodite/dist/aphrodite.umd.min.js?compression=gzip&label=gzip%20size
664[size-badge]: http://img.badgesize.io/https://unpkg.com/aphrodite/dist/aphrodite.umd.min.js?label=size
665[unpkg-dist]: https://unpkg.com/aphrodite/dist/