Skip to content

Guía ​

En esta sección, instalaremos EffCSS y veremos cómo usarlo.

Instalar ​

Escribe en tu terminal:

sh
# npm
npm i effcss

# pnpm
pnpm add effcss

# yarn
yarn add effcss

Tipado CSS ​

Por defecto, EffCSS es permisivo: los estilos aceptan cualquier objeto. Para habilitar un tipado CSS estricto (autocompletado y validación de valores no válidos), define EffCSS.Properties una sola vez en tu proyecto mediante la ampliación del espacio de nombres global.

Por ejemplo, puedes instalar csstype

sh
npm i -D csstype

Luego, combínalo con EffCSS

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

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

export {};

Ahora, cada regla de estilo cuenta con autocompletado completo de TypeScript para las propiedades CSS.

Usar ​

Para crear estilos con EffCSS, solo necesitas llamar a las utilidades. Afortunadamente, son muy pocas y su función se entiende fácilmente por su nombre.

Entre todas las utilidades, cabe destacar classNames y attributes. Estas requieren especificar un contrato en forma de un tipo TypeScript con el que se implementarán los selectores. Esto te permitirá controlar tanto la creación como el uso de los estilos. Un tipo de contrato es un objeto con cualquier nivel de anidamiento de propiedades.

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;
};

Otras utilidades derivan los tipos de sus argumentos. Analicemos con más detalle el uso de cada una de ellas.

classNames ​

className crea una única regla CSS con el contenido especificado y devuelve el selector de clase como una cadena:

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 (o className.lazy) se diferencia de className en los siguientes aspectos:

  • puede aceptar tanto un objeto como una función como argumentos;
  • devuelve una función que resuelve el selector;
  • genera CSS tras el primer uso del resolvedor devuelto.
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 crea una hoja de estilos y devuelve una función para derivar nombres de clases:

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 se diferencia de classNames en que ejecuta la función pasada y crea una hoja de estilo después de que los selectores se derivan por primera vez:

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 crea una única regla CSS con el contenido especificado y devuelve el selector de atributo como un objeto:

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 (o attribute.lazy) se diferencia de attribute en los siguientes aspectos:

  • puede aceptar tanto un objeto como una función como argumentos;
  • devuelve una función que resuelve el selector;
  • genera CSS tras el primer uso del resolvedor devuelto.
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 crea una hoja de estilo y devuelve una función para derivar atributos:

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 se diferencia de attributes en que ejecuta la función pasada y crea una hoja de estilo después de que los selectores se derivan por primera vez:

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>
};

customStyles ​

customStyles crea una hoja de estilos sin selectores derivados:

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 se diferencia de customStyles en que ejecuta la función pasada y crea una hoja de estilos después de la primera llamada al resultado:

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 crea una única regla CSS @property, variables crea varias a la vez:

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'
        }
    };
});

Puedes obtener y establecer el valor inicial de una variable global utilizando los métodos correspondientes:

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

Los métodos set/get para variables locales son funciones auxiliares, ya que estas variables se crean dentro de una hoja de estilo independiente y aún no existen cuando se ejecuta la función generadora. Por lo tanto, estos métodos solo se pueden llamar en variables globales.

animations ​

animation crea una única regla CSS @keyframes, animations crea varias a la vez:

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 crea una única regla CSS @layer, layers crea varias a la vez:

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 crea una única regla CSS @container, containers crea varias a la vez:

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 crea una única regla CSS @font-face, fonts crea varias a la vez:

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 agrupa variables y sus valores en variantes de tema para simplificar el cambio dinámico de estilos:

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 actualiza el valor inicial de la(s) variable(s) global(es):

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'
});

Utilice el método set de la variable siempre que sea posible, ya que es más explícito.

stylesheet ​

stylesheet devuelve la hoja de estilos creada:

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

También existen utilidades que devuelven hojas de estilo especiales:

  • layersStylesheet devuelve una hoja de estilo para capas globales,
  • variablesStylesheet devuelve una hoja de estilo para variables globales,
  • animationsStylesheet devuelve una hoja de estilo para animaciones globales,
  • fontsStylesheet devuelve una hoja de estilo para fuentes globales,
  • sharedStylesheet devuelve una hoja de estilo con reglas globales (creadas mediante className y attribute).

configure ​

configure afecta a la generación de estilos si se llama antes de que se cree la primera hoja de estilos:

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 convierte su argumento o todas las hojas de estilo creadas en una cadena HTML:

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

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

serializeMeta serializa los metadatos de su argumento o los metadatos de todas las hojas de estilo creadas en una cadena HTML:

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();

De esta forma, los estilos y metadatos se pueden calcular en el servidor y reutilizar en el cliente. Esto resulta especialmente útil para SSG/SSR.

subscribe ​

subscribe permite escuchar eventos de creación de estilo EffCSS:

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();

Esta es una herramienta de depuración y control de grano fino, por lo que no debe usarse dentro de una compilación de producción.

Publicado bajo la Licencia Apache 2.0