blakeembrey / free-style

Make CSS easier and more maintainable by using JavaScript
MIT License
707 stars 29 forks source link
css css-in-js css-string dedupe hash javascript js minification typescript

Free-Style

Free-Style is designed to make CSS easier and more maintainable by using JavaScript.

Installation

npm install free-style --save

Why?

There's a great presentation by Christopher Chedeau you should check out.

Solved by using CSS in JS

Also solved with Free-Style

But How?

Free-Style generates a hash from the style to use as the class name. This allows duplicate styles to automatically be merged on duplicate hashes. Every style is "registered" and assigned to a variable, which gets the most out of linters that will warn on unused variables and features like dead code minification. Styles should usually be created outside of the application run loop (e.g. render()) so the CSS string and hashes are only generated once.

Ways to Use

Usage

import { create } from "free-style";

// Create a stylesheet instance.
const sheet = create();

// Register a new style, returning a class name to use.
const backgroundStyle = sheet.registerStyle({
  backgroundColor: "red",
}); //=> "f14svl5e"

// Inject `<style>` into the `<head>`.
const styleElement = document.createElement("style");
styleElement.textContent = sheet.getStyles();
document.head.appendChild(styleElement);

// Render the style by using the class name.
React.render(
  <div className={backgroundStyle}>Hello world!</div>,
  document.body,
);

Style

const buttonStyle = sheet.registerStyle({
  $displayName: "button",
  backgroundColor: "red",
  padding: 10,
});

console.log(buttonStyle); //=> "button_f65pi0b"

Tip: The string returned by registerStyle is a unique hash of the content and used as the HTML class name. The $displayName is only used during development, and stripped in production (process.env.NODE_ENV === 'production').

Overload CSS properties

Style.registerStyle({
  background: [
    "red",
    "-moz-linear-gradient(left, red 0%, blue 100%)",
    "-webkit-linear-gradient(left, red 0%, blue 100%)",
    "-o-linear-gradient(left, red 0%, blue 100%)",
    "-ms-linear-gradient(left, red 0%, blue 100%)",
    "linear-gradient(to right, red 0%, blue 100%)",
  ],
}); //=> "f1n85iiq"

Nested rules

Style.registerStyle({
  color: "red",
  "@media (min-width: 500px)": {
    //=> "@media (min-width: 500px){.fk9tfor{color:blue}}"
    color: "blue",
  },
}); //=> "fk9tfor"

Nested selectors

Style.registerStyle({
  color: "red",
  ".classy": {
    //=> ".fc1zv17 .classy"
    color: "blue",
  },
}); //=> "fc1zv17"

Parent selector

Style.registerStyle({
  color: "red",
  "&:hover": {
    //=> ".f1h42yg6:hover"
    color: "blue",
  },
}); //=> "f1h42yg6"

Tip: The ampersand (&) will be replaced by the parent selector at runtime.

Use JavaScript

const ellipsisStyle = {
  whiteSpace: "nowrap",
  overflow: "hidden",
  textOverflow: "ellipsis",
};

const redEllipsisStyle = Style.registerStyle({
  color: "red",
  ...ellipsisStyle,
}); //=> "fvxl8qs"

// Share rule between styles using computed properties.
const mediaQuery = "@media (min-width: 400px)";

const style = Style.registerStyle({
  backgroundColor: "red",
  [mediaQuery]: {
    backgroundColor: "pink",
  },
});

Unique style output

Sometimes you need to skip the de-duping behavior of free-style. Use $unique to force separate styles:

Style.registerStyle({
  color: "blue",
  "&::-webkit-input-placeholder": {
    color: `rgba(0, 0, 0, 0)`,
    $unique: true,
  },
  "&::-moz-placeholder": {
    color: `rgba(0, 0, 0, 0)`,
    $unique: true,
  },
  "&::-ms-input-placeholder": {
    color: `rgba(0, 0, 0, 0)`,
    $unique: true,
  },
}); //=> "f13byakl"

Style.getStyles(); //=> ".f13byakl{color:blue}.f13byakl::-webkit-input-placeholder{color:rgba(0, 0, 0, 0)}.f13byakl::-moz-placeholder{color:rgba(0, 0, 0, 0)}.f13byakl::-ms-input-placeholder{color:rgba(0, 0, 0, 0)}"

Rules

const colorAnimation = Style.registerStyle({
  $global: true,
  "@keyframes &": {
    from: { color: "red" },
    to: { color: "blue" },
  },
}); //=> "h1j3ughx"

const style = Style.registerStyle({
  animationName: colorAnimation,
  animationDuration: "1s",
}); //=> "fibanyf"

Global rules and styles

Style.registerStyle({
  $global: true,
  "@font-face": {
    fontFamily: '"Bitstream Vera Serif Bold"',
    src: 'url("https://mdn.mozillademos.org/files/2468/VeraSeBd.ttf")',
  },
});

Style.registerStyle({
  $global: true,
  "@media print": {
    body: {
      color: "red",
    },
  },
});

Style.registerStyle({
  $global: true,
  body: {
    margin: 0,
    padding: 0,
  },
});

Style.registerStyle({
  $global: true,
  body: {
    margin: 0,
    padding: 0,
    "@print": {
      color: "#000",
    },
  },
  h1: {
    fontSize: "2em",
  },
});

CSS string

Style.getStyles(); //=> ".f65pi0b{background-color:red;padding:10px}"

TypeScript and ESM

This package is a pure ESM package and ships with TypeScript definitions. It cannot be require'd or used with CommonJS module resolution in TypeScript.

Useful libraries

Implementation details

Debugging

Display names will automatically be removed when process.env.NODE_ENV === "production".

Changes

The only argument to create() is a map of change function handlers. All functions are required:

All styles implement Container, so you can call getStyles() or clone().

Merging

Sheet, Style, and Rule have the ability to be merged.

const otherSheet = create();

sheet.merge(otherSheet); // Merge the current styles of `otherSheet` into `sheet`.
sheet.unmerge(otherSheet); // Remove the current styles of `otherSheet` from `sheet`.

Pre-process styles

If you plan to re-use styles across Sheets, it will be more efficient to use compile once and register many times instead of registerStyle.

License

MIT