UNPKG

15.8 kBMarkdownView Raw
1# astroturf
2
3**astroturf** lets you write CSS in your JavaScript files without adding any runtime layer, and with your existing CSS processing pipeline.
4
5- **Zero runtime CSS-in-JS.** Get many of the same benefits as CSS-in-JS, but without the loss of flexibility in requiring framework-specific CSS processing, and while keeping your CSS fully static with no runtime style parsing.
6- Use your existing tools – **Sass, PostCSS, Less** – but still write your style definitions in your JavaScript files
7- **Whole component in the single file**. Write CSS in a template literal, then use it as if it were in a separate file
8
9<!-- START doctoc generated TOC please keep comment here to allow auto update -->
10<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
11
12
13- [Usage](#usage)
14- [Component API](#component-api)
15- [`css` prop](#css-prop)
16- [Component API Goals and Non-Goals](#component-api-goals-and-non-goals)
17- [Composition, variables, etc?](#composition-variables-etc)
18- [Referring to other Components](#referring-to-other-components)
19- [Sharing values between styles and JavaScript](#sharing-values-between-styles-and-javascript)
20- [Keyframes and global](#keyframes-and-global)
21- [Attaching Additional Props](#attaching-additional-props)
22- [`as` prop](#as-prop)
23- [Setup](#setup)
24 - [Options](#options)
25 - [Use with Gatsby](#use-with-gatsby)
26 - [Use with Parcel](#use-with-parcel)
27 - [Use with Preact](#use-with-preact)
28 - [Use with Next.js](#use-with-nextjs)
29 - [Use without webpack](#use-without-webpack)
30
31<!-- END doctoc generated TOC please keep comment here to allow auto update -->
32
33## Usage
34
35```js
36import React from 'react';
37import { css } from 'astroturf';
38
39const styles = css`
40 .button {
41 color: black;
42 border: 1px solid black;
43 background-color: white;
44 }
45`;
46
47export default function Button({ children }) {
48 return <button className={styles.button}>{children}</button>;
49}
50```
51
52When processed, the `css` block will be extracted into a `.css` file, taking advantage of any and all of the other loaders configured to handle css.
53
54It even handles statically analyzable interpolations!
55
56```js
57import { css } from 'astroturf';
58
59const margin = 10;
60const height = 50;
61const bottom = height + margin;
62
63const styles = css`
64 .box {
65 height: ${height}px;
66 margin-bottom: ${margin}px;
67 }
68
69 .footer {
70 position: absolute;
71 top: ${bottom}px;
72 }
73`;
74```
75
76## Component API
77
78For those that want something a bit more like styled-components or Emotion, there is a component API!
79
80```js
81import styled, { css } from 'astroturf';
82
83const Button = styled('button')`
84 color: black;
85 border: 1px solid black;
86 background-color: white;
87
88 &.primary {
89 color: blue;
90 border: 1px solid blue;
91 }
92
93 &.color-green {
94 color: green;
95 }
96`;
97```
98
99You can render this with:
100
101```js
102render(
103 <Button primary color="green">
104 A styled button
105 </Button>,
106 mountNode,
107);
108```
109
110The above transpiles to something like:
111
112```js
113const styles = css`
114 .button {
115 color: black;
116 border: 1px solid black;
117 background-color: white;
118
119 &.primary {
120 color: blue;
121 border: 1px solid blue;
122 }
123
124 &.color-green {
125 color: green;
126 }
127 }
128`;
129
130function Button({ primary, color, className, ...props }) {
131 return (
132 <button
133 {...props}
134 className={classNames(
135 className,
136 styles.button,
137 primary && styles.primary,
138 color === 'green' && styles.colorGreen,
139 )}
140 />
141 );
142}
143```
144
145## `css` prop
146
147In addition to the `styled` helper, styles can be defined directly on components via the `css` prop.
148You first need to enable this feature via the `enableCssProp` option in your loader config
149
150```jsx
151function Button({ variant, children }) {
152 return (
153 <button
154 variant={variant}
155 css={css`
156 color: black;
157 border: 1px solid black;
158 background-color: white;
159
160 &.variant-primary {
161 color: blue;
162 border: 1px solid blue;
163 }
164
165 &.variant-secondary {
166 color: green;
167 }
168 `}
169 >
170 {children}
171 </button>
172 );
173}
174```
175
176Styles are still extracted to a separate file, any props matching other defined classes are passed to the `classNames()` library. At runtime `styled()` returns a React component with the static CSS classes applied. You can check out the ["runtime"](https://github.com/4Catalyzer/astroturf/blob/master/src/runtime/styled.js) it just creates a component.
177
178There are a whole bucket of caveats of course, to keep the above statically extractable, and limit runtime code.
179
180- We assume you are using css-modules in your css pipeline to return classes from the style files, we don't do any of that ourselves.
181- Prop value handling requires the nesting transform
182- All "top level" styles have any @import statements hoisted up (via a regex)
183
184## Component API Goals and Non-Goals
185
186The goal of this API is not to mimic or reimplement the features of other css-in-js libraries, but to provide
187a more ergonomic way to write normal css/less/sass next to your javascript.
188
189What does that mean? css-in-js libraries are a _replacement_ for css preprocessors, in that they provide ways of doing variables, composition, mixins, imports etc. Usually they accomplish this by leaning
190on JS language features where appropriate, and adding their own domain-specific language bits when needed.
191
192astroturf **doesn't try to do any of that** because it's not trying to replace preprocessors, but rather, make component-centric javascript work better with **existing** styling tooling. This means at a minimum it needs to scope styles to the component (handled by css-modules) and map those styles to your component's API (props), which is what the above API strives for.
193
194This approach **gains** us:
195
196- No additional work to extract styles for further optimization (autoprefixer, minifying, moving them to a CDN, etc)
197- The smallest runtime, it's essentially zero
198- Blazing Fast™ because there is zero runtime evaluation of styles
199- Leverage the well-trod and huge css preprocesser ecosystems
200
201It also means we **sacrifice**:
202
203- A fine-grained style mapping to props. Props map to classes, its all very BEM-y but automated
204- Dynamism in sharing values between js and css
205- A unified JS-only headspace, you still need to think in terms of JS and CSS
206
207## Composition, variables, etc?
208
209How you accomplish that is mostly up to your preprocessor. Leverage Sass variables, or Less mixins, or postcss nesting polyfills, or whatever. The css you're writing is treated exactly like a normal style file so all the tooling you're used to works as expected. For composition, specifically around classes, you can also use css-modules `composes` to compose styles and interpolation;
210
211```js
212// Button.js
213
214const helpers = css`
215 .heavy {
216 font-weight: 900;
217 }
218`;
219
220const Title = styled('h3')`
221 composes: ${helpers.heavy};
222
223 font-size: 12%;
224`;
225```
226
227You don't have to define everything in a `.js` file. Where it makes sense just use normal css (or any other file type).
228
229```scss
230// mixins.scss
231@mixin heavy() {
232 font-weight: 900;
233}
234```
235
236and then:
237
238```js
239// Button.js
240const Title = styled('h3')`
241 @import './mixins.scss';
242
243 @include heavy();
244 font-size: 12%;
245`;
246```
247
248## Referring to other Components
249
250One limitation to fully encapsulated styles is that it's hard to contextually style components
251without them referencing each other. In astroturf you can use a component in a
252selector as if it were referencing a class selector.
253
254> Note: Referencing stylesheets or styled components from other files has a few caveats:
255> [cross-file-dependencies](/docs/cross-file-dependencies.md)
256
257```js
258const Link = styled.a`
259 display: flex;
260 align-items: center;
261 padding: 5px 10px;
262 background: papayawhip;
263 color: palevioletred;
264`;
265
266const Icon = styled.svg`
267 flex: none;
268 transition: fill 0.25s;
269 width: 48px;
270 height: 48px;
271
272 ${Link}:hover & {
273 fill: rebeccapurple;
274 }
275`;
276```
277
278## Sharing values between styles and JavaScript
279
280We've found that in practice, you rarely have to share values between the two, but there are times when it's
281very convenient. Astroturf ofters two ways to do this, the first is string interpolations.
282
283```js
284const DURATION = 500;
285
286const ColorTransition = styled('nav')`
287 color: red;
288 transition: color ${DURATION}ms;
289
290 &.blue {
291 color: blue;
292 }
293`;
294
295class App extends React.Component {
296 state = { blue: false }
297 toggle = () => {
298 this.setState(s => ({ blue: !s.blue }), () => {
299 setTimeout(() => console.log('done!'), DURATION)
300 })
301 }
302 render() {
303 const { blue } = this.state
304 <div>
305 <ColorTransition blue={blue} />
306 <button onClick={this.toggle}>Toggle Color</button>
307 </div>;
308 }
309}
310```
311
312This works great for local variables, since the compiler can determine their
313value at compile time and share them. For cases when you want to share things a bit more globally, such as in a theme, we recommend leaning on your css preprocesser again.
314
315css-modules provides a syntax for exporting values from styles, generally this is used for class names, but you can leverage it for whatever values you want. Combined with something like Sass's variables it ends up being a powerful tool.
316
317```js
318const breakpointValues = css`
319 @import '../styles/theme';
320
321 :export {
322 @each $breakpoint, $px in $grid-breakpoints {
323 #{$breakpoint}: $px;
324 }
325 }
326`
327
328class Responsive extends React.Component {
329 state = { isMobile: false }
330
331 componentDidMount() {
332 this.setState({
333 isMobile: window.clientWidth < parseInt(breakpoints.md, 10)
334 })
335 }
336
337 render() {
338 const { isMobile } = this.state
339 <div>
340 {isMobile ? 'A small screen!' : 'A big screen!'}
341 </div>;
342 }
343}
344```
345
346## Keyframes and global
347
348Everything in `css` will be used as normal CSS Modules styles.
349So, if you need to insert some CSS without isolation (like reset with [postcss-normalize](https://github.com/csstools/postcss-normalize)):
350
351```js
352css`
353 @import-normalize;
354
355 :global(.btn) {
356 background: blue;
357 }
358`;
359```
360
361With [postcss-nested](https://github.com/postcss/postcss-nested) you can
362add keyframes to specific component (and keyframes name will not be global):
363
364```js
365const Loader = styled('div')`
366 animation-name: rotation;
367 animation-duration: 1s;
368 animation-timing-function: linear;
369 animation-iteration-count: infinite;
370
371 @keyframes rotation {
372 to {
373 transform: rotate(360deg);
374 }
375 }
376`;
377```
378
379## Attaching Additional Props
380
381A common task with styled components is to map their props or set default values.
382astroturf cribs from Styled Components, by including an `attrs()` api.
383
384```jsx
385import styled from 'astroturf';
386
387// Provide a default `type` props
388const PasswordInput = styled('input').attrs({
389 type: 'password',
390})`
391 background-color: #ccc;
392`;
393
394// Map the incoming props to a new set of props
395const TextOrPasswordInput = styled('input').attrs(
396 ({ isPassword, ...props }) => ({
397 ...props,
398 type: isPassword ? 'password' : 'text',
399 }),
400)`
401 background-color: #ccc;
402`;
403```
404
405Because `attrs()` is resolved during render you can use hooks in them! We even
406do some magic in the non-function signature so that it works.
407
408```js
409const Link = styled('a').attrs(props => ({
410 href: useRouter().createHref(props.to)
411}))`
412 color: blue;
413`);
414
415// astroturf will automatically compile to a function
416// when using a plain object so that the hooks
417// are only evaluated during render
418const Link = styled(MyLink).attrs({
419 router: useRouter()
420})`
421 color: blue;
422`);
423```
424
425## `as` prop
426
427`astroturf` supports the `as` prop to control the underlying element type at runtime.
428
429```js
430const Button = styled('button')`
431 color: red;
432`
433
434<Button as="a" href="#link"/>
435```
436
437**This feature is only enabled by default for host components**, e.g. native DOM elements. We do this to prevent annoying conflicts with other UI libraries like react-bootstrap or semantic-ui which also use the the `as` prop. If you want to enable it for any styled component you can do so via the `allowAs` option.
438
439```js
440const StyledFooter = styled(Footer, { allowAs: true })`
441 color: red;
442`;
443```
444
445## Setup
446
447If you want the simplest, most bare-bones setup you can use the included `css-loader` which will setup css-modules and postcss-nested. This is the minimum setup necessary to get it working. Any options passed to the loader are passed to the official webpack `css-loader`
448
449```js
450{
451 module: {
452 rules: [
453 {
454 test: /\.css$/,
455 use: ['style-loader', 'astroturf/css-loader'],
456 },
457 {
458 test: /\.jsx?$/,
459 use: ['babel-loader', 'astroturf/loader'],
460 },
461 // astroturf works out of the box with typescript (.ts or .tsx files).
462 {
463 test: /\.tsx?$/,
464 use: ['ts-loader', 'astroturf/loader'],
465 },
466 ];
467 }
468}
469```
470
471You can add on here as you would normally for additional preprocesser setup. Here's how you might set up Sass:
472
473```js
474{
475 module: {
476 rules: [
477 {
478 test: /\.module\.scss$/,
479 use: ['style-loader', 'astroturf/css-loader', 'sass-loader'],
480 },
481 {
482 test: /\.jsx?$/,
483 use: [
484 'babel-loader',
485 {
486 loader: 'astroturf/loader',
487 options: { extension: '.module.scss' },
488 },
489 ],
490 },
491 ],
492 }
493}
494```
495
496You can also skip the included `css-loader` entirely if your preprocessor handles nesting out of the box (like most do).
497
498```js
499[
500 {
501 test: /\.module\.scss$/,
502 use: ['style-loader', 'css-loader?modules=true', 'sass-loader'],
503 },
504 ...
505]
506```
507
508### Options
509
510astroturf accepts a few query options.
511
512- **tagName**: (default: `'css'`) The tag identifier used to locate inline css literals and extract them.
513- **styledTag**: (default: `'styled'`) The tag identifier used to locate components.
514- **extension**: (default: `'.css'`) the extension used for extracted "virtual" files. Change to whatever file type you want webpack to process extracted literals as.
515- **enableCssProp**: (default: false) compiles `css` props to styled components.
516
517**Note:** astroturf expects uncompiled JavaScript code, If you are using babel or Typescript to transform tagged template literals, ensure the loader runs _before_ babel or typescript loaders.
518
519### Use with Parcel
520
521Add these lines to `package.json` to work with [Parcel](https://parceljs.org/) builder:
522
523```json
524 "postcss": {
525 "modules": true,
526 "plugins": [
527 "postcss-nested"
528 ]
529 },
530 "babel": {
531 "plugins": [
532 "astroturf/plugin"
533 ]
534 },
535```
536
537### Use with Gatsby
538
539See [gatsby-plugin-astroturf](https://github.com/silvenon/gatsby-plugin-astroturf)
540
541
542### Use with Preact
543
544Add these lines to `package.json` to work with [Preact](https://preactjs.com/):
545
546```json
547 "browser": {
548 "react": "preact"
549 },
550```
551
552### Use with Next.js
553
554See [example](https://github.com/zeit/next.js/tree/canary/examples/with-astroturf)
555
556### Use without webpack
557
558If you aren't using webpack and still want to define styles inline, there is a babel plugin for that.
559
560Config shown below with the default options.
561
562```js
563// babelrc.js
564module.exports = {
565 plugins: [
566 [
567 'astroturf/plugin',
568 {
569 tagName: 'css',
570 extension: '.css',
571 writeFiles: true, // Writes css files to disk using the result of `getFileName`
572 getFileName(hostFilePath, pluginsOptions) {
573 const basepath = join(
574 dirname(hostFilePath),
575 basename(hostFilePath, extname(hostFilePath)),
576 );
577 return `${basepath}__extracted_style${opts.extension}`;
578 },
579 },
580 ],
581 ],
582};
583```
584
585The extracted styles are also available on the `metadata` object returned from `babel.transform`.
586
587```js
588const { metadata } = babel.transformFile(myJsfile);
589
590metadata['astroturf'].styles; // [{ path, value }]
591```