es-in-css
This library serves much of the same purpose as SASS/SCSS, LESS and other CSS
preprocessors, but uses plain JavaScript/TypeScript to provide type-safety and
"programmability" with local variables, mixins, utility functions, etc. etc.
Overall this is a "do less" toolkit with tiny API, that mainly tries to stay
out of your way.
SCSS-like selector nesting, inline
// comments
support
and autoprefixer features are
automatically provided by postCSS
, but apart from that it's all pretty
basic. Just you composing CSS.
For good developer experience, use VSCode and install the official
vscode-styled-components extension. That gives
you instant syntax highlighting and IntelliSense autocompletion inside
css``
template literals, and maybe add a few
helpful "snippets".
See also the chapter
Why es-in-css Instead of SASS? below.
Table of Contents:
Quick-Start Guide
yarn add --dev es-in-css
Create a file called src/cool-design.css.js
:
import { css, makeVariables, px } from 'es-in-css';
const colors = {
yellow: `yellow`,
red: `#cc3300`,
purple: `#990099`,
};
const bp = { large: 850 };
const mq = {
small: `screen and (max-width: ${px(bp.large - 1)})`,
large: `screen and (min-width: ${px(bp.large)})`,
};
const cssVars = makeVariables([
'linkColor',
'linkColor--hover',
'linkColor__focus',
'focusColor',
]);
const vars = cssVars.vars;
export default css`
:root {
${cssVars.declare({
linkColor: colors.red,
'linkColor--hover': colors.purple, // dashes must be quoted
linkColor__focus: var.focusColor, // aliased
focusColor: `peach`,
})}
}
a[href] {
color: ${vars.linkColor};
unknown-property: is ok;
&:hover {
color: ${vars['linkColor--hover']};
}
&:focus-visible {
color: ${vars.linkColor__focus};
}
}
@media ${mq.large} {
html {
background-color: ${colors.yellow};
}
}
`;
Then build/compile the CSS file with the command:
yarn run es-in-css "src/*.css.js" --outdir=dist/styles
or using npm:
npm exec es-in-css "src/*.css.js" --outdir=dist/styles
You now have a file called dist/styles/cool-design.css
:
:root {
--linkColor: #cc3300;
--linkColor--hover: #990099;
--linkColor__focus: var(--focusColor);
--focusColor: peach;
}
a[href] {
color: var(--linkColor);
unknown-property: is ok;
}
a[href]:hover {
color: var(--linkColor--hover);
}
a[href]:focus-visible {
color: var(--linkColor__focus);
}
@media screen and (min-width: 850px) {
html {
background-color: yellow;
}
}
CSS Authoring Features
The es-in-css
module exports the following methods:
css
Templater
Syntax: css`...`: string
Dumb(-ish) tagged template literal that returns a string
. It provides nice
syntax highlighting and code-completion in VSCode by using a well-known name.
Example of use:
import { css } from 'es-in-css';
const themeColors = {
yellow: `yellow`,
red: `#cc3300`,
purple: `#990099`,
};
const textColor = `#333`;
const boxStyle = () => css`
background: #f4f4f4;
border: 1px solid #999;
border-radius: 3px;
padding: 12px;
`;
export default css`
body {
color: ${textColor};
${boxStyle};
}
${Object.entries(themeColors).map(
(color) => css`
body.theme--${color.name} {
background-color: ${color.value};
}
`
)}
`;
Depsite being quite "dumb" it does have some convenience features:
- It filter away/suppresses "falsy" values (except
0
) similar to how React
behaves. - Arrays are falsy-filtered and then auto-joined with a space.
- Bare functions are invoked without any arguments.
All other values are cast to string as is.
cssVal
Templater
Syntax: Same as css
Templater
An alias for the css``
templater, for cases where you're writing a
standalone CSS value or other out-of-context CSS snippets, and you wish to
disable VSCode's syntax highlighting and error checking.
str
Quoted String Printer
Syntax: str(value: string): string
Helper to convert a value to a quoted string.
Example:
import { str, css } from 'es-in-css';
const message = 'Warning "Bob"!';
export default css`
.foo::before {
content: ${str(message)};
}
`;
scoped
Name Generator
Syntax: scoped(prefix?: string): string
Returns a randomized/unique string token, with an optional prefix
. These
tokens can be using for naming @keyframes
or for mangled class-names, if
that's what you need:
import { scoped, css } from 'es-in-css';
export const blockName = scoped(`Button`);
export default css`
.${blockName} {
border: 1px solid blue;
}
.${blockName}__title {
font-size: 2rem;
}
`;
Unit Value Helpers
Fixed/Physical sizes: px()
and cm()
Font relative: em()
, rem()
, ch()
and ex()
Layout relative: pct()
(%), vh()
, vw()
, vmin()
and vmax()
Time: ms()
Angle: deg()
These return light-weight UnitValue
instances that can behave as either
string or number literals, depending on the context.
(See the unitVal
helper for more info)
import { px, css } from 'es-in-css';
const leftColW = px(300);
const mainColW = px(700);
const gutter = px(50);
const totalWidth = px(leftColW + gutter + mainColW);
export default css`
.layout {
width: ${totalWidth};
margin: 0 auto;
display: flex;
gap: ${gutter};
}
.main {
width: ${mainColW};
}
.sidebar {
width: ${leftColW};
}
`;
unitVal
Helper
Syntax:
unitVal<U extends string>(value: number | UnitValue<U>, unit: U): UnitValue<U> & number
Creates a custom UnitValue
instance that is also number
-compatible (see
Unit Value Types for more info)
import { unitVal, px } from 'es-in-css';
const valA = unitVal(10, 'px');
const valB = px(10);
valA.value === 10;
valA.unit === 'px';
valB.value === 10;
valB.unit === 'px';
`${valA}` === '10px';
`${valB}` === '10px';
valA * 2 === 20;
valB * 2 === 20;
const numA = valA;
const numB = valB;
const minVal = Math.min(valA, valB);
typeof valB === 'number';
Unit Value Types
The unit value helpers emit the following UnitValue
sub-types:
PxValue
, RemValue
, EmValue
, ChValue
, ExValue
, PctValue
, VwValue
,
VhValue
, VminValue
, VmaxValue
, MsValue
, CmValue
, DegValue
,
FrValue
All of these unit types are also typed as a number
, to tell TypeScript that
the values are safe to use in calculations. (They are safe because they have a
number-returning .valueOf()
method.)
NOTE: This white "lie" about the number
type may cause problems at
runtime if these "UnitNumbers" end up in situations where
typeof x === "number"
is used to validate a literal number value.
However, the risk vs. benefit trade-off seems reasonable.
For cases that require an actual non-unit, plain number
value, you can use
the PlainNumber
type. Example:
import { PlainNumber, PxValue, rem } from 'es-in-css';
export const pxRem = (px: PlainNumber | PxValue) => rem(px / 16);
Additionally, there are helpful categorized union types:
LayoutRelativeValue
– all container proportional units: %
, vw
, etc.FontRelativeValue
– all text proportional units: em
, rem
, etc.LengthValue
– all fixed/physical units (px
, cm
), plus the above
unions.
Unit Converters
To keep it simple and sane es-in-css
only supports one UnitValue
type
per category of units (time, angles, physical size, etc.) but provides
friendly converter functions from other units of measure into the main
supported units.
Percentage values from proportions/fractions:
pct_f()
, vh_f()
, vw_f()
, vmin_f()
and vmax_f()
.
pct_f(1 / 3);
vw_f(370 / 1400);
Milliseconds from seconds:
ms_sec()
ms_sec(1.2);
Centimeters from other physical units:
cm_in()
, cm_mm()
, cm_pt()
and cm_pc()
.
cm_mm(33.3);
cm_in(1);
Degrees from other angle units:
deg_turn()
, deg_rad()
, deg_grad()
,
deg_turn(0.75);
deg_rad(-Math.PI);
unitOf
Helper
Syntax:
unitOf<U extends string>(value: number | UnitValue<U>): U | undefined
Checks if its given argument is a UnitValue
instance and returns its .unit
property.
import { unitOf } from 'es-in-css';
unitOf(px(10));
unitOf(ms_sec(1));
Returns undefined
otherwise.
unitOf(10);
unitOf('10px');
Color Helper
es-in-css
bundles the color
package
and re-exports it as color
.
The color class/function creates ColorValue
instances that can be used in
CSS, but also come with useful manipulation mhethods.
import { color, css } from 'es-in-css';
const c1 = color('red');
const c2 = c1.fade(0.8).desaturate(0.5);
export default css`
div {
color: ${c1};
background-color: ${c2};
}
`;
It extends color
by adding a static fromName
method to generate a
type-safe color-name to ColorValue
mapper:
const prettyColor = color.fromName('lime');
const prettyColor2 = color('lime');
const notAColor2 = color('bogus');
const notAColor = color.fromName('bogus');
It also exports rgb()
and hsl()
which are simple aliases of the color
package's static class methods of the same names.
import { rgb, hsl, color } from 'es-in-css';
const rgbRed = rgb(255, 0, 0);
const hslRed = hsl(0, 100, 50);
const rgbRedFaded = rgb(255, 0, 0, 0.5);
const hslRedFaded = hsl(0, 100, 50, 0.5);
rgb === color.rgb;
hsl === color.hsl;
Feel free to import your own color helper library, and use it instead.
makeVariables
Helper
Syntax:
makeVariables<T extends string>(variableTokens: Array<T>, options?: VariableOptions): VariableStyles<T>
Helper to provide type-safety and code-completion when using CSS custom
properties (CSS variables) at scale.
See VariableOptions
below for configuration options.
import { makeVariables, css } from 'es-in-css';
const myVarNames = ['linkColor', 'linkColor__hover'];
const cssVars = makeVariables(myVarNames);
The returned VariableStyles
object contains the following properties:
VariableStyles.vars.*
— pre-declared CSS value printers (outputting
var(--*)
strings)VariableStyles.declare(…)
— for declaring initial values for all of the
variables.VariableStyles.override(…)
— for re-declaraing parts of the variable
collection.
VariableStyles.vars
Syntax: VariableStyles<T>.vars: Record<T, VariablePrinter>
Holds a readonly Record<T, VariablePrinter>
object where the
VariablePrinter
s emit the CSS variable names wrapped in var()
, ready to be
used as CSS values … with the option of passing a default/fallback value via
the .or() method
.
const { vars } = cssVars;
vars.linkColor + '';
vars.linkColor.or(`black`);
`color: ${vars.linkColor__hover};`;
VariablePrinter
objects also have a cssName
property with the raw
(unwrapped) name of the variable, like so:
vars.linkColor.cssName;
VariableStyles.declare
Syntax: VariableStyles<T>.declare(vars: Record<T, string >): string
Lets you type-safely write values for all the defined CSS variables into a
CSS rule block. Property names not matching T
are dropped/ignored.
css`
:root {
${cssVars.declare({
linkColor: `#0000cc`,
linkColor__hover: `#cc00cc`,
unknown_variable: `transparent`, // ignored/dropped
})}
}
`;
VariableStyles.override
Syntax:
VariableStyles<T>.override(vars: Partial<Record<T, string >>): string
Similar to the .declare()
method, but can be used to re-declare (i.e.
override) only some of of the CSS variables T
. Again, property names not
matching T
are ignored/dropped.
Furthermore, values of null
, undefined
, false
are interpreted as
"missing", and the property is ignored/dropped.
css`
@media (prefers-color-scheme: dark) {
:root {
${cssVars.override({
linkColor: `#9999ff`,
unknown_variable: `#transparent`, // ignored/dropped
linkColor__hover: false, // ignored/dropped
})}
}
}
`;
makeVariables.join
Composition Helper
Syntax:
makeVariables.join(...varDatas: Array<VariableStyles>): VariableStyles
This helper combines the variable values and declaration methods from multiple
VariableStyles
objects into a new, larger VariableStyles
object.
const colorVariables = makeVariables(['primary', 'secondary', 'link'], {
namespace: 'color-',
});
const fontVariables = makeVariables(['heading', 'normal', 'smallprint'], {
namespace: 'font-',
});
const allVariables = makeVariables.join(colorVariables, fontVariables);
css`
p {
color: ${allVariables.vars.primary};
font: ${allVariables.vars.normal};
}
`;
makeVariables.isVar
Helper
Syntax: makeVariables.isVar(value: unknown): value is VariablePrinter
A helper that checks if an input value is of type VariablePrinter
.
import { makeVariables } from 'es-in-css';
makeVariables.isVar(cssVars.vars.linkColor);
makeVariables.isVar('var(--linkColor)');
makeVariables.isVar('' + cssVars.vars.linkColor);
makeVariables.isVar(cssVars);
VariableOptions
By default only "simple" ascii alphanumerical variable-names are allowed
(/^[a-z0-9_-]+$/i
). If unsuppored/malformed CSS variable names are passed,
the function throws an error. However, you can author your own RegExp
to
validate the variable names, and a custom CSS variable-name mapper:
VariableOptions.nameRe?: RegExp
Custom name validation RegExp, for if you want/need to allow names more
complex than the default setting allows.
(Default: /^[a-z0-9_-]+$/i
)
const var1 = makeVariables(['töff']);
const var2opts: VariableOptions = { nameRe: /^[a-z0-9_-áðéíóúýþæö]+$/i };
const var2 = makeVariables(['töff'], var2opts);
var2.vars.töff + '';
VariableOptions.toCSSName?: (name: string) => string
Maps weird/wonky JavaScript property names to CSS-friendly css custom property
names.
(Default: (name) => name
)
const var3opts: VariableOptions = {
toCSSName: (name) => name.replace(/_/g, '-'),
};
const var3 = makeVariables(['link__color'], var3opts);
var3.declare({ link__color: 'blue' });
var3.vars.link__color + '';
VariableOptions.namespace?: string
Prefix that gets added to all CSS printed variable names.
The namespace is neither validated nor transformed in any way, except that
spaces and other invalid characters are silently stripped away.
const var4opts: VariableOptions = {
namespace: ' ZU{U}PER-',
};
const var4 = makeVariables(['link__color'], var4opts);
var4.declare({ link__color: 'blue' });
var4.vars.link__color + '';
Compilation API
The es-in-css
compiler imports/requires the default string export of the
passed javascript modules and passes it through a series of postcss
plugins
before writing the resulting CSS to disc.
CLI Syntax
The es-in-css
package exposes a CLI script of the same name. (Use yarn run
or npm exec
to run it, unless you have ./node_modules/.bin/
in PATH, or
es-in-css is installed "globally".)
es-in-css "inputglob" --outbase=src/path --outdir=out/path --minify
inputglob
Must be quoted. Handles all the patterns supported by the
glob
module.
-d, --outdir <path>
By default the compiled CSS files are saved in the same folder as the source
file. This is rarely the desired behavior so by setting outdir
you choose
where the compiled CSS files end up.
The output file names replace the input-modules file-extension with .css
—
unless if the source file name ends in .css.js
, in which case the .js
ending is simply dropped.
-b, --outbase <path>
If your inputglob file list contains multiple entry points in separate
directories, the directory structure will be replicated into the outdir
starting from the lowest common ancestor directory among all input entry point
paths.
If you want to customize this behavior, you should set the outbase
path.
-e, --ext <file-extension>
Customize the file-extension of the output files. Default is .css
-m, --minify
Opts into moderately aggressive, yet safe cssnano
minification of the resulting CSS.
All comments are stripped, except ones that start with /*!
.
-p, --prettify [configFilePath]
Runs the result CSS through Prettier. Accepts optional configFilePath
, but
defaults to resolving .prettierrc
for --outdir
or the current directory.
Ignored if mixed with --minify
.
-n, --no-nested
Disables the SCSS-like selector nesting behavior provided by the
postcss-nested plugin.
(To pass custom options to the plugin, use the JavaScript API.)
CLI Example Usage
es-in-css "src/css/**/*.js" --outdir=dist/styles
Given the src
folder contained the following files:
src/css/styles.css.js
src/css/resets.js
src/css/component/buttons.css.js
src/css/component/formFields.js
The dist folder now contains:
dist/styles/styles.css
dist/styles/resets.css
dist/styles/component/buttons.css
dist/styles/component/formFields.css
Note how the src/css/
is automatically detected as a reasonable common
ancestor. If you want to make src/
the base folder, you must use the
outbase
option, like so:
es-in-css "src/css/**/*.js" --outbase=src --outdir=dist/styles
The dist folder now contains:
dist/styles/css/styles.css
dist/styles/css/resets.css
dist/styles/css/component/buttons.css
dist/styles/css/component/formFields.css
JS API
The options for the JavaScript API are the same as for the CLI, with the
following additions:
write?: boolean
— (Default: true
) Allows turning off the automatic
writing to disc, if you want to post-process the files and handle the FS
writes manually.
When turned off the CSS content is returned as part of the promise payload.redirect?: (outFile: string, inFile: string) => string | undefined
—
Dynamically changes the final destination of the output files. (Values that
lead to overwriting the source file are ignored.)banner?: string
— Text that's prepended to every output file.footer?: string
— Text that's appended to every output file.ext?: string | (inFile: string) => string | undefined
— The function
signature allows dynamically choosing a file-extension for the output files.nesting?: boolean | import('postcss-nesting').Options
— (Default: true
)
Allows turning off the SCSS-like selector nesting behavior provided by
postcss-nested or passing it
custom options.
compileCSS
(from files)
Works in pretty much the same way as the CLI.
Takes a list of files to read, and returns an Array of result objects each
containing the compiled CSS and the resolved output file path.
const { compileCSS } = require('es-in-js/compiler');
const { writeFile } = require('fs/promise');
const files = [
'src/foo/styles.css.js',
'src/foo/styles2.css.js',
]
compileCSS(sourceFiles, {
outbase: 'src'
outdir: 'dist'
write: false,
}).then((result) => {
console.log(result.inFile);
writeFile(result.outFile, result.css);
});
compileCSSFromJS
Compiles CSS from a JavaScript source string. This may be the preferable
method when working with bundlers such as esbuild
.
(NOTE: This method temporarily writes the script contents to the file system
to allow imports and file-reads to work correctly, but then deletes those
files afterwards.)
const { compileCSSFromJS } = require('es-in-js/compiler');
const { writeFile } = require('fs/promise');
const scriptStrings = [
{
fileName: '_temp/styles.css.mjs',
content: `
import { css } from 'es-in-css';
export const baseColor = 'red';
export default css\`
body { color: \${baseColor}; }
\`;
`,
},
{
fileName: '_temp/styles2.css.mjs',
content: `
import { css } from 'es-in-css';
import { baseColor } from './styles.css.mjs';
export default css\`
div { color: \${baseColor}; }
\`;
`,
},
];
compileCSSFromJS(scriptStrings, {
outbase: 'src',
outdir: 'dist',
write: false,
}).then((result) => {
console.log(result.inFile);
writeFile(result.outFile, result.css);
});
compileCSSString
Lower-level method that accepts a raw, optionally nested, CSS string (or an
array of such strings) and returns a compiled CSS string (or array) —
optionally minified or prettified.
const { compileCSSString } = require('es-in-js/compiler');
const rawCSS = `
// My double-slash comment
body {
p { color: red;
> span { border:none }
}
}
`;
compileCSSString(rawCSS, {
prettify: true,
footer: '/* The "footer" is appended as is */',
}).then((outCSS) => {
console.log(outCSS);
});
Why es-in-css Instead of SASS?
TL;DR: JavaScript/TypeScript provides better developer ergonomics than
SASS, and is a more future-proof technology.
SASS has been almost an industry standard tool for templating CSS code for
well over a decade now. Yet it provides poor developer experience with
lackluster editor integrations, idiosyncratic syntax, extremely limited
feature set, publishing and consuming libraries is hard, etc…
Over the past few years, the web development community has been gradually
moving on to other, more nimble technologies — either more vanilla "text/css"
authoring, or class-name-based reverse compilers like Tailwind, or various
CSS-in-JS solutions.
This package provides supportive tooling for this last group, but offers also
a new lightweight alternative: To author CSS using JavaScript as a templating
engine, and then output it via one of the following methods:
writeFile
the resulting string to static file- Use an es-to-css compiler,
- Stream it directly to the browser,
- Use some build tool "magic" (e.g. write a custom Webpack loader)
Helpful VSCode Snippets
Here are a few code "snippets" you can
add to your global snippets file
to help you use es-in-css a bit faster:
"Insert ${} variable print block": {
"scope": "javascript,javascriptreact,typescript,typescriptreact,css",
"prefix": "v",
"body": "\\${$0}",
},
"css`` tagged template literal": {
"scope": "javascript,javascriptreact,typescript,typescriptreact",
"prefix": "css",
"body": "css`\n\t$0\n`",
},
"cssVal`` tagged template literal": {
"scope": "javascript,typescript,typescriptreact",
"prefix": "cssVal",
"body": "cssVal`\n\t$0\n`",
},
"New *.css.js file": {
"scope": "javascript,typescript",
"prefix": "css-js-file",
"body": [
"import { css } from 'es-in-css';",
"",
"export default css`\n\t$0\n`"],
},
Also make sure you install the official vscode-styled-components
extension for fancy syntax highlighting and
IntelliSense autocompletion inside css``
template literals
Roadmap
- Loaders/config for Webpack, esbuild, Next.js builds, etc. (Help wanted!)
Maybes:
- Add more CSS authoring helpers/utilities (ideas/PRs welcome)
- In-built
--watch
mode (may be out of scope?) - Ability to add more postcss plugins and more fine-grained plugin
configurability.
- Compilation directly from TypeScript
Not planned:
- Emitting source maps.
- Complicated config files, etc.
Changelog
See CHANGELOG.md