Skip to content

Guide ​

In this section, we'll install EffCSS and see how to use it.

Install ​

Type in your terminal:

sh
# npm
npm i effcss

# pnpm
pnpm add effcss

# yarn
yarn add effcss

CSS types ​

By default EffCSS is permissive: styles accept any object. To enable strict CSS-typing (autocompletion + validation of invalid values), define EffCSS.Properties once in your project via global namespace augmentation.

For example, you can install csstype

sh
npm i -D csstype

Then combine with EffCSS

ts
// env.d.ts
import type { Properties as CSSProperties } from 'csstype';

declare global {
    namespace EffCSS {
        interface Properties extends CSSProperties {}
    }
}

export {};

Now every style rule gets full TS autocompletion for CSS properties.

Use ​

To create styles with EffCSS, you just need to call the utilities. Fortunately, there are very few, and by their name you can understand what they do.

Among all the utilities, you need to highlight classNames and attributes. They require specifying a contract in the form of a Typescript type with which selectors will be implemented. This will allow you to control both the creation of styles and their use. A contract type is an object with any level of nesting properties:

ts
/**
 * Components stylesheet
 */
type Components = {
    /**
     * Is rounded
     */
    rounded: true;
    /**
     * Height
     */
    h: 'full' | 'half';
    /**
     * Card
     */
    card: {
        /**
         * Card background
         */
        bg: 'primary' | 'secondary';
        /**
         * Is card disabled
         */
        disabled: boolean;
        
    };
    /**
     * Spinner component
     */
    spinner: {};
};

/**
 * Utils stylesheet
 */
type Utils = {
    /**
     * Width
     */
    w: 's' | 'm' | 'l';
    /**
     * Spacing
     */
    spacing: 0 | 1 | 2;
    /**
     * Blink animation
     */
    blink: true;
};

Other utilities derive types from their arguments. Let's take a closer look at the usage of each of them.

classNames ​

className creates a single CSS rule with the specified content and returns the class selector as a string:

tsx
import { className } from 'effcss';

// create
const cls = className({
    margin: 'auto',
    '&:hover': {
        outline: '2px solid black',
        '.child': {
            background: 'grey'
        }
    }
});

// apply
export const Component = () => {
    return <div className={cls}>
        Card
    </div>
};

lazyClassName (or className.lazy) differs from className in the following ways:

  • it can accept both an object and a function as an argument;
  • it returns a function that resolves the selector;
  • it generates CSS after the first use of the returned resolver.
tsx
import { lazyClassName } from 'effcss';

// create
const resolveCls = lazyClassName(/* the same or function */);
// or className.lazy(/* the same */)
// the rule has not been created yet

const cls = resolveCls();
// the rule has been created

// apply
export const Component = () => {
    return <div className={cls}>
        Card
    </div>
};

classNames creates a stylesheet and returns a function for deriving classnames:

tsx
import { classNames } from 'effcss';

// declare
type Card = {
    w: 's' | 'm' | 'l';
    blur: true;
    card: {
        variant: 1 | 2;
        rounded: true;
    };
}

// implement
const card = classNames<Card>((selectors) => {
    const {w, card, blur} = selectors;
    return {
        [w.s]: {
            width: '12px'
        },
        [w.m]: {
            width: '24px'
        },
        [w.l]: {
            width: '26px'
        },
        [blur.true]: {
            filter: 'blur(5px)'
        },
        [card]: {
            background: 'white',
            border: 'none'
        },
        [card.variant[1]]: {
            width: 'auto',
            display: 'block',
            padding: '12px',
            '&:hover': {
                cursor: 'pointer'
            }
        },
        [card.variant[2]]: {
            width: 'auto',
            display: 'flex',
            flexDirection: 'column',
            padding: '16px',
            '&:hover': {
                outline: '2px solid black'
            }
        },
        [card.rounded.true]: {
            borderRadius: '1rem'
        }
    }
});

const cls = card({
    card: {
        rounded: true
    },
    w: 's'
});

// apply
export const Component = () => {
    return <div className={cls}>
        Card
    </div>
};

lazyClassNames (or alternatively classNames.lazy) differs from classNames in that it performs the passed function and creates a stylesheet after the selectors are derived for the first time:

tsx
import { lazyClassNames } from 'effcss';

// declare
type Card = {/* the same */};

// implement
const card = lazyClassNames<Card>(/* the same */);
// or classNames.lazy<Card>(/* the same */);
// the stylesheet has not been created yet

const cls = card({
    card: {
        rounded: true
    },
    w: 's'
});
// the stylesheet has been created

// apply
export const Component = () => {
    return <div className={cls}>
        Card
    </div>
};

attributes ​

attribute creates a single CSS rule with the specified content and returns the attribute selector as an object:

tsx
import { attribute } from 'effcss';

// create
const attr = attribute({
    margin: 'auto',
    '&:hover': {
        outline: '2px solid black',
        '.child': {
            background: 'grey'
        }
    }
});

// apply
export const Component = () => {
    return <div {...attr}>
        Card
    </div>
};

lazyAttribute (or attribute.lazy) differs from attribute in the following ways:

  • it can accept both an object and a function as an argument;
  • it returns a function that resolves the selector;
  • it generates CSS after the first use of the returned resolver.
tsx
import { lazyAttribute } from 'effcss';

// create
const resolveAttr = lazyAttribute(/* the same or function */);
// or attribute.lazy(/* the same */)
// the rule has not been created yet

const attr = resolveAttr();
// the rule has been created

// apply
export const Component = () => {
    return <div {...attr}>
        Card
    </div>
};

attributes creates stylesheet and returns a function for deriving attributes:

tsx
import { attributes } from 'effcss';

// declare
type Card = {
    w: 's' | 'm' | 'l';
    blur: true;
    card: {
        variant: 1 | 2;
        rounded: true;
    };
}

// implement
const card = attributes<Card>((selectors) => {
    const {w, card, blur} = selectors;
    return {
        [w.s]: {
            width: '12px'
        },
        [w.m]: {
            width: '24px'
        },
        [w.l]: {
            width: '26px'
        },
        [blur.true]: {
            filter: 'blur(5px)'
        },
        [card]: {
            background: 'white',
            border: 'none'
        },
        [card.variant[1]]: {
            width: 'auto',
            display: 'block',
            padding: '12px',
            '&:hover': {
                cursor: 'pointer'
            }
        },
        [card.variant[2]]: {
            width: 'auto',
            display: 'flex',
            flexDirection: 'column',
            padding: '16px',
            '&:hover': {
                outline: '2px solid black'
            }
        },
        [card.rounded.true]: {
            borderRadius: '1rem'
        }
    }
});

const attrs = card({
    card: {
        rounded: true
    },
    w: 's'
});

// apply
export const Component = () => {
    return <div {...attrs}>
        Card
    </div>
};

lazyAttributes (or alternatively attributes.lazy) differs from attributes in that it performs the passed function and creates a stylesheet after the selectors are derived for the first time:

tsx
import { lazyAttributes } from 'effcss';

// declare
type Card = {/* the same */};

// implement
const card = lazyAttributes<Card>(/* the same */);
// or attributes.lazy<Card>(/* the same */);
// the stylesheet has not been created yet

const attrs = card({
    card: {
        rounded: true
    },
    w: 's'
});
// the stylesheet has been created

// apply
export const Component = () => {
    return <div {...attrs}>
        Card
    </div>
};

id ​

id creates a single CSS rule with the specified content and returns the id selector as a string:

tsx
import { id } from 'effcss';

// create
const myId = id({
    margin: 'auto',
    '&:hover': {
        outline: '2px solid black',
        '.child': {
            background: 'grey'
        }
    }
});

// apply
export const Component = () => {
    return <div id={myId}>
        Card
    </div>
};

lazyId (or id.lazy) differs from id in the following ways:

  • it can accept both an object and a function as an argument;
  • it returns a function that resolves the selector;
  • it generates CSS after the first use of the returned resolver.
tsx
import { lazyId } from 'effcss';

// create
const resolveId = lazyId(/* the same or function */);
// or id.lazy(/* the same */)
// the rule has not been created yet

const myId = resolveId();
// the rule has been created

// apply
export const Component = () => {
    return <div id={myId}>
        Card
    </div>
};

customStyles ​

customStyles creates stylesheet without derived selectors:

tsx
import { customStyles } from 'effcss';

// implement
customStyles(() => ({
    '.custom': {
        background: 'transparent',
        width: '100%',
        '&:hover': {
            outline: '2px solid black'
        }
    },
    '@media screen and (max-width: 768px)': {
        '.custom': {
            width: '50%'
        }
    }
}));

// apply
export const Component = () => {
    return <div className='custom'>
        Card
    </div>
};

lazyCustomStyles differs from customStyles in that it performs the passed function and creates a stylesheet after the first result call:

tsx
import { lazyCustomStyles } from 'effcss';

// implement
const applyStyles = lazyCustomStyles(/* the same */);
// the stylesheet has not been created yet

applyStyles();
// the stylesheet has been created

// apply
export const Component = () => {
    return <div className='custom'>
        Card
    </div>
};

variables ​

variable creates a single CSS @property rule, variables creates several at once:

ts
import { customStyles, variable, variables } from 'effcss';

// global
const offset = variable('10px');
const colors = variable({
    primary: {
        syntax: 'color',
        inherits: false,
        initialValue: '#2192a7'
    },
    secondary: '#425158'
});

customStyles(() => {
    // local
    const localOffset = variable({
        inherits: true,
        initialValue: '12px'
    });
    const localColors = variables({
        primary: '#2192a7',
        secondary: '#425158'
    });
    
    return {
        '.global': {
            background: colors.primary(),
            // with fallback value
            padding: offset('8px'),
        },
        '.local': {
            // with fallback value
            background: localColors.primary('grey'),
            padding: localOffset()
        },
        '.override': {
            [localColors.primary]: 'grey'
        }
    };
});

You can get and set the initial value of a global variable using the corresponding methods:

ts
const offset = variable('10px');
const colors = variable({
    primary: {
        syntax: 'color',
        inherits: false,
        initialValue: '#2192a7'
    },
    secondary: '#425158'
});

offset.set('14px');
colors.primary.set('grey');
colors.secondary.set('green');

const actualOffsetValue = offset.get();
const actualPrimaryColorValue = colors.primary.get();

INFO

The set/get methods for local variables are stubs, since these variables are created within a separate stylesheet and do not yet exist when the generator function is executed. Therefore, these methods can only be called on global variables.

animations ​

animation creates a single CSS @keyframes rule, animations creates several at once:

ts
import { customStyles, animation, animations } from 'effcss';

// global
const spin = animation({
    from: {
        transform: 'rotate(0deg)',
    },
    to: {
        transform: 'rotate(360deg)',
    },
});
const blink = animations({
    simple: {
        '50%': {
            visibility: 'hidden'
        }
    },
    smooth: {
        '0%': {
            opacity: 1
        },
        '50%': {
            opacity: 0
        },
        '100%': {
            opacity: 1
        }
    }
});

customStyles(() => {
    // local
    const localSpin = animation(/* the same */);
    const localBlink = animations(/* the same */);
    
    return {
        '.global-spin': {
            animation: `${spin} 6s infinite`
        },
        '.global-blink': {
            animation: `${blink.smooth} 2s infinite`
        },
        '.local-spin': {
            animation: `${localSpin} 6s infinite`
        },
        '.local-blink': {
            animation: `${localBlink.smooth} 2s infinite`
        },
    };
});

layers ​

layer creates a single CSS @layer rule, layers creates several at once:

ts
import { customStyles, layer, layers } from 'effcss';

// global
const single = layer();
const list = layers(['theme', 'layout', 'utilities']);

customStyles(() => {
    // local
    const localSingle = layer();
    const localList = layers(['theme', 'layout', 'utilities']);
    
    return {
        [single]: {
            '.global-layer': {
                background: 'transparent'
            }
        },
        [list.theme]: {
            '.global-layer': {
                background: '#425158'
            }
        },
        [localSingle]: {
            '.local-layer': {
                background: 'white'
            }
        },
        [localList.theme]: {
            '.local-layer': {
                background: 'grey'
            }
        }
    };
});

containers ​

container creates a single CSS @container rule, containers creates several at once:

ts
import { customStyles, container, containers } from 'effcss';

// global
const single = container();
const multiple = containers({
    normal: '',
    inline: 'inline-size',
    scrollState: 'size scroll-state'
});

customStyles(() => {
    // local
    const localSingle = container();
    const localMultiple = containers({
        normal: '',
        inline: 'inline-size',
        scrollState: 'size scroll-state'
    });
    
    return {
        '.global-container': {
            container: single()
        },
        [single + ' not scroll-state(scrollable: none)']: {
            '.inside-global-container': {
                width: '100%'
            }
        },
        '.local-container': {
            container: localMultiple.inline()
        },
        [localMultiple.inline + ' (max-width: 768px)']: {
            '.inside-local-container': {
                width: '100%'
            }
        },
    };
});

fonts ​

font creates a single CSS @font-face rule, fonts creates several at once:

ts
import { customStyles, font, fonts } from 'effcss';

// global
const single = font({
    src: `url("https://mdn.github.io/shared-assets/fonts/FiraSans-Regular.woff2")`,
    genericName: 'sans-serif'
});
const multiple = fonts({
    primary: {
        src: `url("/fonts/roboto-regular.woff2") format("woff2"), url("/fonts/roboto-regular.woff") format("woff")`,
        weight: 400,
        style: 'normal',
        display: 'swap'
    },
    secondary: {
        src: `url("https://mdn.github.io/shared-assets/fonts/FiraSans-Regular.woff2")`
    }
});

customStyles(() => {
    // local
    const localSingle = font(/* the same */);
    const localMultiple = fonts(/* the same */);
    
    return {
        '.global-font': {
            fontFamily: single()
        },
        '.global-primary-font': {
            // `single` as fallback
            fontFamily: multiple.primary(single)
        },
        '.local': {
            fontFamily: localSingle()
        },
        '.local-primary-font': {
            // `localSingle` as fallback
            fontFamily: localMultiple.primary(localSingle)
        }
    };
});

theme ​

theme groups variables and their values ​​into theme variants to simplify dynamic styles switching:

ts
import { theme } from 'effcss';

const colorTheme = theme({
    vars: {
        bg: { syntax: 'color', inherits: false, initialValue: '#fff' },
        fg: { syntax: 'color', inherits: false, initialValue: '#111' },
        accent: { syntax: 'color', inherits: true, initialValue: '#2192a7' }
    },
    options: {
        light: { bg: '#fff', fg: '#111', accent: '#2192a7' },
        dark: { bg: '#111', fg: '#eee', accent: '#7fd0ff' }
    },
    initial: 'light'
});

// get current theme variant
colorTheme.get();
// set next theme variant
colorTheme.set('dark');
// get all theme variants
colorTheme.list;
// use theme vars
colorTheme.vars.bg();

update ​

update refreshes initial value of global variable/variables:

ts
const offset = variable('10px');
const colors = variable({
    primary: {
        syntax: 'color',
        inherits: false,
        initialValue: '#2192a7'
    },
    secondary: '#425158'
});

update(offset, '14px');
update(colors, {
    primary: 'grey',
    secondary: 'green'
});

Use the variable set method whenever possible, as it is more explicit.

stylesheet ​

stylesheet returns the created stylesheet:

ts
const custom = customStyles(() => {
    return {
        '.custom': {
            padding: '1rem'
        },
    };
});
// specified stylesheet
const customStylesheet = stylesheet(custom);

There are also utilities that return special stylesheets:

  • layersStylesheet returns a stylesheet for global layers,
  • variablesStylesheet returns a stylesheet for global variables,
  • animationsStylesheet returns a stylesheet for global animations,
  • fontsStylesheet returns a stylesheet for global fonts,
  • sharedStylesheet returns a stylesheet of global rules (created using className and attribute).

configure ​

configure affects style generation if it is called before the first stylesheet is created:

ts
configure({
    // custom prefix for ids
    prefix: 'custom',
    // disable minification
    minify: false,
    // emulate server-side mode
    emulate: true,
    // enable lazy mode
    lazy: true
});

serialize ​

serialize converts its argument or all created stylesheets to an HTML string:

ts
const custom = customStyles(() => {
    return {
        '.custom': {
            padding: '1rem'
        },
    };
});
// specified stylesheet
const customHTML = serialize(custom);

// all created stylesheets
const fullHTML = serialize();

serializeMeta serializes the metadata of its argument or the metadata of all created stylesheets into an HTML string:

ts
import { classNames } from 'effcss';

type Card = {/* the same */};

const card = classNames<Card>(/* the same */);

// specified stylesheet metadata
const cardMetaHTML = serializeMeta(card);

// all created stylesheets metadata
const fullMetaHTML = serialize();

This way, styles and metadata can be calculated on the server and reused on the client. This is especially useful for SSG/SSR.

subscribe ​

subscribe allows you to listen to EffCSS style creation events:

ts
import { subscribe, classNames } from 'effcss';

const events = [];
// start listen to events
const unsubscribe = subscribe((event) => events.push(event));

type Card = {/* the same */};
const card = classNames<Card>(/* the same */);

// stop listen to events
unsubscribe();

This is a fine-grained control and debugging tool, so it should not be used inside a production build.

Released under the Apache-2.0 License.