• Stars
    star
    185
  • Rank 208,271 (Top 5 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 3 years ago
  • Updated 11 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

styleQ is a quick, small JavaScript runtime for merging the HTML class names produced by CSS compilers.

styleQ Β· GitHub license npm version Build Status PRs Welcome

styleQ is a quick, small JavaScript runtime for merging the HTML class names produced by CSS compilers.

  • High performance merging for initial render.
  • Built-in memoization for updates.
  • Merges static and dynamic styles.
  • Supports various CSS compiler designs.
  • 0.7 KB gzipped runtime.

Use

Install:

npm install styleq

Import:

import { styleq } from 'styleq';

API

styleq(...styles)

Merges style objects and produces a DOM className string and inline style object (camelCase property names).

const [ className, inlineStyle ] = styleq(styles.root, { opacity });

The styleq function efficiently merges deeply nested arrays of both extracted and inline style objects.

  • Compiled styles must set the $$css property to true.
  • Any style object without the $$css property is treated as an inline style.
  • Compiled styles must be static for best performance.
  • Compiled style object keys do not need to match CSS property names; any string is allowed.
  • Compiled style object values must be an HTML class string.
/* Generated output */

const styles = {
  root: {
    // Needed by the runtime
    $$css: true,
    // Debug classes
    'debug::file:styles.root': 'debug::file:styles.root',
    // Atomic CSS classes
    display: 'display-flex-class',
    alignItems: 'alignItems-center-class'
  }
};

const [ className, inlineStyle ] = styleq(styles.root, props.style);

styleq.factory(options) => styleq

A factory for creating custom merging functions, tailored to the design of specific compilers.

const compilerStyleq = styleq.factory(options);

Options are used to configure the merging function.

type Options = {
  // control memoization
  disableCache: boolean = false;
  // control className/style merge strategy
  disableMix: boolean = false;
  // transform individual styles at runtime before merging
  transform: ?(style) => compiledStyle;
}

disableCache

Memoization is enabled by default. This option can be used to disable it. Memoization relies on a tree of WeakMaps keyed on static compiled styles. This allows the runtime to efficiently store chunks of merged styles, making re-computation very cheap. However, checking the WeakMap for memoized data when there is none significantly adds to the cost of initially computing the result. Therefore, if initial computations need to be as fast as possible (e.g., your use case involves few repeat merges), memoization should be disabled.

const styleqNoCache = styleq.factory({ disableCache: true });

disableMix

Inline styles are merged together with static styles by default, but can be merged independently if preferred. Both static and inline styles can be passed to styleq for merging. By default, the properties defined by static and inline styles are merged together. The performance of this option is still excellent, but merging with inline styles often means memoization cannot be used as effectively. In certain circumstances, this merging strategy can result in better performance, as the deduplication of styles can reduce the number of CSS rules applied to the element (which improves browser layout times).

If mixing is diabled, the static and inline styles will be treated as values for different attributes: either className OR style respectively. If an inline style sets a property that is later set by a static style, both the static class name and dynamic style property will be set. In practice this means that inline style declarations override those of static styles, whatever their position in the styles array passed to styleq. Therefore, memoization of class name merges is not changed by inline styles, and so provides the best general performance.

const styleqNoMix = styleq.factory({ disableMix: true });

transform

Styles can be transformed before merging by using the transform function. The runtime loop is extremely performance sensitive as class name merges can happen 1000s of times during a screen render, whether on the server or client. The transform function is used to change style objects before styleQ merges them. For example, if a compiler needs runtime information before selecting a compiled style.

// compiler/useStyleq
import { styleq } from 'styleq';
import { localizeExtractedStyle } from './localizeExtractedStyle';
import { useLocaleContext } from './useLocaleContext'
import { useMemo } from 'react';

export function useStyleq(styles) {
  // Runtime context provides subtree writing direction
  const { direction } = useLocaleContext();
  const isRTL = direction === 'rtl';
  // Create a custom styleq for localization transform
  const styleqWithPolyfills = useMemo(
    () => styleq.factory({
      transform(style) {
        // Memoize results in the transform
        return localizeExtractedStyle(style, isRTL);
      }
    }),
    [ isRTL ]
  );
  const styleProps = styleqWithPolyfills(styles);
  // Add vendor prefixes to inline styles
  if (styleProps[1]) {
    styleProps[1] = prefixAll(styleProps[1]);
  }
  return styleProps;
}

WARNING: Transforming compiled styles to support runtime dynamism is possible without negatively effecting performance, however, transforms must be done carefully to avoid creating merge operations that cannot be efficiently memoized. WeakMap is recommended for memoizing the result of transforms, so that static objects are passed to styleq.

Notes for compiler authors

CSS compilers implementing different styling models can all target styleQ to deliver excellent runtime performance. styleQ can be used at build time and runtime (for server and client) to generate className and style values.

Examples of how various compiler features and designs can supported with styleQ are discussed below.

Supporting zero-conflict styles

Zero-conflict styles provide developers with guarantees that component style is encapsulated and not implicitly altered by styles defined by other components. A compiler designed around zero-conflict styles will generally output "atomic CSS" and produce smaller CSS style sheets that avoid all specificity and source order conflicts.

Typically, a zero-conflict design involves excluding support for descendant selectors (i.e., any selector that targets an element other than the element receiving the class name). And shortform properties are either disallowed, restricted, or automatically expanded to longform properties. If pseudo-classes (e.g., :focus) are supported, the compiler must guarantee the order of precedence between pseudo-classes in the CSS style sheet (e.g., :focus rules appear before :active rules). If Media Queries are supported, they too must be carefully ordered.

Input:

import * as compiler from 'compiler';

const styles = compiler.create({
  root: {
    margin: 10,
    opacity: 0.7,
    ':focus': {
      opacity: 0.8
    },
    ':active': {
      opacity: 1.0
    }
  }
});

Output:

insertOrExtract('.margin-left-10 { margin-left:10px; }', 0);
insertOrExtract('.margin-top-10 { margin-top:10px; }', 0);
insertOrExtract('.margin-right-10 { margin-right:10px; }', 0);
insertOrExtract('.margin-bottom-10 { margin-bottom:10px; }', 0);
insertOrExtract('.opacity-07 { opacity:0.7; }', 0);
// Pseudo-class insertion order is after class selector rules
insertOrExtract('.focus-opacity-08:focus { opacity:0.8; }', 1.0);
insertOrExtract('.active-opacity-1:active { opacity:1; }', 1.1);

const styles = {
  root: {
    $$css: true,
    marginLeft: 'margin-left-10',
    marginTop: 'margin-top-10',
    marginRight: 'margin-right-10',
    marginBottom: 'margin-bottom-10',
    opacity: 'opacity-07',
    __focus$opacity: 'focus-opacity-08',
    __active$opacity: 'active-opacity-1'
  }
};

Supporting arbitrary selectors

The runtime can be used by compilers that support arbitrary selectors, e.g., by concatenating (hashing, etc.) the selector string and property to create a unique key for that selector-property combination. (Note that supporting arbitrary CSS selectors trades flexibility for zero-conflict styles.)

Input:

import * as compiler from 'compiler';

const styles = compiler.create({
  root: {
    ':focus a[data-prop]': {
      opacity: 1
    }
  }
});

Output:

insertOrExtract('.xjrodmsp-opacity-1:focus a[data-prop] { opacity:1.0; }');

const styles = {
  root: {
    $$css: true,
    '__xjrodmsp-opacity-1': 'xjrodmsp-opacity-1'
  }
};

Supporting high-performance layouts

Atomic CSS has tradeoffs. Once an element has many HTML class names each pointing to different CSS rules, browser layout times slow down. In some cases, compilers may choose to flatten multiple declarations into "traditional" CSS. For example, a component library may optimize the "reset" styles for its core components by flattening those styles, and then inserting those rules into the CSS style sheet before all the atomic CSS. That way atomic CSS will always override the reset rules, and the layout performance of the core components will be significantly improved.

Input:

import { createResetStyle } from 'compiler';

function View(props) {
  return (
    <div {...props} css={reset, props.css} />
  );
}

const reset = createResetStyle({
  display: 'flex',
  alignItems: 'stretch',
  flexDirection: 'row',
  ...
});

Output:

import { styleq } from 'compiler/styleq';

// Compiler inserts Reset CSS rules before Atomic CSS rules.
insertOrExtract('.reset-<hash> { display:flex; align-items:stretch; flex-direction:row', 0);

function View(props) {
  const [ className, inlineStyle ] = styleq(reset, props.css);
  return (
    <div {...props} className={className} style={inlineStyle} />
  );
}

const reset = {
  $$css: true,
  // Compiler decides that only one reset is allowed per element.
  // Each reset rule created is set to the '__reset' key.
  __reset: 'reset-<hash>',
};

Supporting high-performance inline styles

A compiler may provide a single API for defining static and dynamic values, and maximize the number of compiled styles by replacing dynamic values with unique CSS custom properties that are then set by inline styles. This compiler design decouples static and inline property merges, and makes the best use of runtime memoization.

Input:

// @jsx createElement
import { createElement } from 'compiler';

function Fade(props) {
  return (
    <div
      {...props}
      css={{
        // static value
        backgroundColor: 'blue',
        // dynamic value
        opacity: props.opacity,
        ...props.css
      }}
    />
  );
}

Output:

// Custom styleq with mixing disabled
import { customStyleq } from 'compiler/customStyleq';

// The opacity value is a unique CSS custom property
insertOrExtract('.backgroundColor-blue { background-color:blue; }');
insertOrExtract('.opacity-var-xyz { opacity:var(--opacity-xyz); }');

// A compiled style is generated, including the 'opacity' property
const compiledStyle = {
  $$css: true,
  backgroundColor: 'backgroundColor-blue',
  opacity: 'opacity-var-xyz'
};

function Fade(props) {
  const [ className, style ] = customStyleq(
    // The dynamic value is set to the custom property.
    // With static/dynamic mixing disabled, the position of the inline style
    // is irrelevant. However, with mixing enabled, the best performance is
    // achieved by placing inline styles earlier in the queue.
    { '--opacity-xyz': props.opacity },
    compiledStyle,
    props.css
  );

  return (
    <div
      {...props}
      className={className}
      style={style}
    />
  );
}

Supporting themes

Compilers implementing themes via CSS custom properties should avoid creating atomic CSS rules for each theme property. As mentioned above, this can slow down browser layout and flattening theme styles into a single rule is preferred. Theme classes can be deduplicated by using the same key for all themes in the generated style object.

Input:

import * as compiler from 'compiler';

const [themeVars, themeStyle] = compiler.createDefaultTheme({
  color: {
    primary: '#fff',
    secondary: '#f5d90a'
    ...
  },
  space: {},
  size: {}
});

const className = compiler.merge(themeStyle, props.style);

Output:

import { styleq } from 'compiler/styleq';

insertOrExtract(
  ':root, .theme-default { --theme-default-color-primary:#fff; --theme-default-color-secondary:#f5d90a; }'
);

const themeVars = {
  color: {
    primary: 'var(--theme-default-color-primary)',
    secondary: 'var(--theme-default-color-secondary)'
    ...
  }
};

const themeStyle = {
  $$css: true,
  __theme: 'theme-default'
};

const [ className ] = styleq(themeStyle, props.style);

Polyfilling logical properties and values

A compiler might provide a polyfill for CSS logical properties and values. Using the transform option is one way to implement this functionality.

Input:

import { StyleSheet } from 'compiler';

function Box() {
  return <div style={styles.root} />
}

const styles = StyleSheet.create({
  root: {
    float: 'inline-start',
  }
});

Output:

// See the 'useStyleq' example in the API docs above
import { useStyleq } from 'compiler/useStyleq';

insertOrExtract('.float-left { float:left; }');
insertOrExtract('.float-right { float:right; }');

function Box() {
  const [ className ] = useStyleq(styles.view, styles.root);
  return <div className={className} />
}

const styles = {
  root: {
    $$css: true,
    // Compiler defines a custom key to mark this style object
    // for processessing by the localized transform.
    $$css$localize: true,
    // [ LTR, RTL ]
    float: [ 'float-left', 'float-right' ]
  }
}

In this case, useStyleq is a function defined by the compiler which transforms $$css styles into the correct class name for a given writing direction. An example of this transform can be seen in the localizeStyle module. Note how care is taken to memoize the transform so that the same result is always used in merges.

Implementing utility styles

Compilers that produce "utility" CSS rules can use styleQ to dedupe utilities across categories, i.e., higher-level styling abstractions such as "size", "spacing", "color scheme", etc. The keys of extracted styles can match the utility categories.

Input:

import { oocss } from 'compiler';

const View = (props) => (
  // This compiler targets strings for named "utilities"
  <div {...props} className={oocss('cs-1 p-1 s-1', props.css)} />
);

const StyledView = (props) => (
  <View {...props} css={oocss('cs-2 p-2')} />
);

Output:

import { styleq } from 'styleq';

insertOrExtract('.cs-1 { --primary-color:#000; --secondary-color:#eee }', 2);
insertOrExtract('.cs-2 { --primary-color:#fff; --secondary-color:#333 }', 2);
insertOrExtract('.p-1 { padding:10px }', 0);
insertOrExtract('.p-2 { padding:20px }', 0);
insertOrExtract('.s-1 { height:100px; width:100px }', 1);

// Each utility class is categorized. For example, only a single 'colorScheme'
// rule will be applied to each element.
const oocss1 = {
  $$css: true,
  __cs: 'cs-1',
  __p: 'p-1',
  __s: 's-1'
};

const View = (props) => (
  <div {...props} className={styleq(oocss1, props.css)} />
);

const oocss2 = {
  $$css: true,
  __cs: 'cs-2',
  __p: 'p-2'
}

const StyledView = (props) => (
  <View {...props} css={oocss2} />
);

License

styleq is MIT licensed.

More Repositories

1

normalize.css

A modern alternative to CSS resets
CSS
52,285
star
2

react-native-web

Cross-platform React UI packages
JavaScript
21,553
star
3

idiomatic-css

Principles of writing consistent, idiomatic CSS.
6,641
star
4

dotfiles

OS X dotfiles: bash, git, vim, etc.
Shell
785
star
5

css3-facebook-buttons

Simple CSS to recreate the appearance of Facebook's buttons and toolbars.
CSS
577
star
6

idiomatic-html

Principles of writing consistent, idiomatic HTML.
525
star
7

css3-social-signin-buttons

CSS3 Social Sign-in Buttons with icons. Small and large sizes.
CSS
475
star
8

issue-guidelines

A set of guidelines for submitting issues and pull requests on projects
426
star
9

griddle

A CSS grid constructor
CSS
367
star
10

icon-builder-example

JavaScript
148
star
11

xdm.js

JSON-RPC 2.0 cross-domain communication over postMessage
JavaScript
89
star
12

emitter.js

A simple JavaScript event-emitter
JavaScript
85
star
13

jekyll-boilerplate

A jekyll starting point.
HTML
73
star
14

generator-jsmodule

A Yeoman generator to scaffold out a simple JS module for Node.js or the browser
JavaScript
68
star
15

chrome-tab-limit

Chrome browser extension that limits you to 20 open tabs
JavaScript
53
star
16

suit-grid-layouts

DEPRECATED
HTML
45
star
17

delegate.js

DOM event delegation component
JavaScript
45
star
18

project-euler

JavaScript solutions to the Project Euler problems.
JavaScript
44
star
19

custom-tweet-button-for-wordpress

A fully customisable HTML and CSS Tweet Button for Wordpress. Built using PHP; no JavaScript requirement. Uses the bit.ly and Twitter APIs.
PHP
36
star
20

dom-shims

Complete and well-tested shims / polyfills for various DOM API's missing in not-so-modern browsers
JavaScript
34
star
21

eraser.css

CSS `!important`-fueled bulldozer
CSS
29
star
22

newsriver-ui

User interface for displaying a river of news generated by Dave Winer's River2 software.
JavaScript
27
star
23

dom-closest

For a given DOM element, find the first ancestor that matches a given CSS selector.
JavaScript
22
star
24

event-shim.js

A W3C DOM Event API shim for IE 8
JavaScript
20
star
25

dom-matches

Check if a DOM element matches a given selector.
JavaScript
19
star
26

dom-insert-html

HTML-string DOM insertion methods using 'insertAdjacentHTML'
JavaScript
13
star
27

matches.js

DEPRECATED
JavaScript
12
star
28

dom-siblings

Find a DOM element's siblings, optionally filtered by a CSS selector.
JavaScript
11
star
29

dom-insert

DOM insertion methods.
JavaScript
10
star
30

dom-responder-events

4
star
31

flight-mustache

A Flight component for rendering Mustache templates with mustache.js
JavaScript
2
star
32

webpack-bugs

JavaScript
1
star