/fusion

Reactive CSS-in-JS

Primary LanguageJavaScriptThe UnlicenseUnlicense

fusion

Version Badge License Build Status

Reactive CSS-in-JS

Install

Download the CJS, ESM, UMD versions or install via NPM:

npm install @ryanmorr/fusion

Usage

Fusion is a tiny CSS-in-JS library that combines declarative CSS building with reactive stores to create data to CSS variable bindings:

import { css, store } from '@ryanmorr/fusion';

const color = store('red');

const style = css`
    .foo {
        color: ${color}
    }
`;

document.head.appendChild(style);

color.set('blue');

API

store(value?)

Create a reactive store that encapsulates a value and can notify subscribers when the value changes:

import { store } from '@ryanmorr/fusion';

// Create a store with an initial value
const count = store(0);

// Get the store value
count.value(); //=> 0

// Set the store value
count.set(1);

// Set the store value with a callback function
count.update((val) => val + 1);

// Subscribe a callback function to be invoked when the value changes,
// it returns a function to unsubscribe from future updates
const unsubscribe = count.subscribe((nextVal, prevVal) => {
    // Do something
});

derived(...stores, callback)

Create a reactive store that is based on the value of one or more other stores:

import { derived, store } from '@ryanmorr/fusion';

const firstName = store('John');
const lastName = store('Doe');
const fullName = derived(firstName, lastName, (first, last) => `${first} ${last}`);

fullName.value(); //=> "John Doe"

firstName.set('Jane');

fullName.value(); //=> "Jane Doe"

// Subscribe to be notified of changes
const unsubscribe = fullName.subscribe((nextVal, prevVal) => {
    // Do something
});

If the callback function defines an extra parameter in its signature, the derived store is treated as asynchronous. The callback function is provided a setter for the store's value and no longer relies on the return value:

import { derived, store } from '@ryanmorr/fusion';

const query = store();

// Perform an ajax request when the query changes
// and notify subscribers with the results
const results = derived(query, (string, set) => {
    fetch(`path/to/server/${encodeURIComponent(string)}`).then(set);
});

css(strings, ...values?)

Create CSS stylesheets declaratively via tagged template literals with support for nested rules:

import { css } from '@ryanmorr/fusion';

// Create a <style> element
const stylesheet = css`
    .foo {
        color: red;

        &:hover {
            color: blue;
        }

        @media (max-width: 750px) {
            & {
                color: purple;
            }
        }

        .bar {
            color: green;
        }
    }

    .baz {
        color: yellow;
    }
`;

// Append styles to document
document.head.appendChild(stylesheet);

Bindings

When a reactive store is interpolated into a css stylesheet, it is replaced with a unique CSS variable bound to that store and will be automatically updated when the internal store value changes:

import { css, store } from '@ryanmorr/fusion';

const width = store('10px');

document.head.appendChild(css`
    .foo {
        width: ${width};
    }
`);

const element = document.querySelector('.foo');

getComputedStyle(element).getPropertyValue('width'); //=> "10px"

width.set('50px');

getComputedStyle(element).getPropertyValue('width'); //=> "50px"

Similarly to stores, promises can also be interpolated into a css stylesheet, setting the value of the binding CSS variable when the promise resolves:

import { html } from '@ryanmorr/fusion';

const height = Promise.resolve('100px');

const style = css`
    .foo {
        height: ${height};
    }
`;

If a store or promise returns a value of null or undefined, the binding CSS variable will be unset.


style(strings, ...values?)

Create styles for an element and its descendants declaratively via tagged template literals and return a unique class name. Just like css, it supports nested rules and interpolating stores and promises:

import { style, store } from '@ryanmorr/fusion';

const color = store('red');

// Create a style declaration and return a class name
const className = style`
    width: 100px;

    &:hover {
        color: white;
    }

    .foo {
        color: ${color};
    }

    @media only screen and (max-width: 30em) {
        & {
            width: 200px;
        }
    }
`;

// Add the unique class to an element
element.classList.add(className);

keyframes(strings, ...values?)

Create a keyframes animation via tagged template literals and return a unique animation name that can be easily applied to a css stylesheet or style class name declaration:

import { keyframes, css } from '@ryanmorr/fusion';

// Create a keyframes animation
const slideIn = keyframes`
    from {
        transform: translateX(0%);
    }
    to {
        transform: translateX(100%);
    }
`;

// Add the animation to a `css` stylesheet
const stylesheet = css`
    .foo {
        animation: ${slideIn} 1s ease-in;
    }
`;

fallback(...values)

Add one or more fallback values for a CSS variable, supporting stores, promises, and CSS variable names. Moving left to right, if the value provided is null or undefined then precedence moves to the next fallback value:

import { fallback, store, css } from '@ryanmorr/fusion';

const store = store();
const promise = Promise.resolve('30px');

document.head.appendChild(css`
    .foo {
        width: ${fallback(store, promise, '--foo', '10px')};
    }
`);

const element = document.querySelector('.foo');

// The 3 previous values are unset, so precedence defaults to the right-most value
getComputedStyle(element).getPropertyValue('width'); //=> "10px"

// When the `--foo` CSS variable is set, it takes precedence
document.documentElement.style.setProperty('--foo', '20px');
getComputedStyle(element).getPropertyValue('width'); //=> "20px"

// Precedence movies to the promise when it resolves
await promise;
getComputedStyle(element).getPropertyValue('width'); //=> "30px"

// Finally, when the reactive store is set, it takes precedence over all the fallback values
store.set('40px');
getComputedStyle(element).getPropertyValue('width'); //=> "40px"

media(mediaQuery)

Create a reactive store for a media query that can also be interpolated into a css stylesheet or style declaration:

import { media, css } from '@ryanmorr/fusion';

// Create the media query store
const smallScreen = media('(max-width: 750px)');

// Returns true if the media query currently matches
const isSmallScreen = smallScreen.value(); //=> true/false

// Interpolate the media query into a stylesheet
const style = css`
    ${smallScreen} {
        .foo {
            color: green;
        }
    }
`;

// Subscribers are called when the status of the media query changes
smallScreen.subscribe((isSmallScreen) => {
    // Do something
});

query(selector)

Create a reactive store for a live array of DOM elements that match a CSS selector string. The store is automatically updated anytime one or more elements matching the CSS selector are added to or removed from the DOM. It can also be interpolated into a css stylesheet or style declaration:

import { query, css } from '@ryanmorr/fusion';

// Create the element store
const fooElements = query('.foo');

// Returns an array of elements that match the CSS selector
const elements = fooElements.value();

// Interpolate the CSS selector into a stylesheet
const style = css`
    ${fooElements} {
        color: yellow;
    }
`;

// Subscribers are called when elements matching the
// CSS selector are added to or removed from the DOM
fooElements.subscribe((nextElements, prevElements) => {
    // Do something
});

DOM

For a DOM-based solution, refer to reflex, a similar library that brings reactivity to elements and attributes. It is also 100% compatible with fusion.

License

This project is dedicated to the public domain as described by the Unlicense.