Skip to content

Руководство ​

В этом разделе мы установим EffCSS и посмотрим, как им пользоваться.

Установка ​

Введите в вашем терминале:

sh
# npm
npm i effcss

# pnpm
pnpm add effcss

# yarn
yarn add effcss

Типизация CSS ​

По умолчанию EffCSS работает в гибком режиме: стили принимают любые объекты. Чтобы включить строгую типизацию CSS (автодополнение и проверку на недопустимые значения), один раз определите EffCSS.Properties в своем проекте, расширив глобальное пространство имен.

Например, можно установить пакет csstype

sh
npm i -D csstype

Затем объедините с EffCSS

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

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

export {};

Теперь для каждого правила стиля доступно полное автодополнение TypeScript для CSS-свойств.

Использование ​

Для создания стилей с помощью EffCSS достаточно воспользоваться функциями-утилитами. К счастью, их совсем немного, а их названия говорят сами за себя.

Все подобные утилиты по умолчанию работают по принципу ленивых вычислений (lazy): стили генерируются только в момент первого использования резолвера (resolver() или resolver + ''). Если стили ссылаются на другие токены, оберните их в функцию () => ({...}) — это позволит сохранить «ленивый» характер их вычисления.

Давайте подробнее рассмотрим, как использовать каждую из них.

id / className ​

id создает одно CSS-правило с указанным содержимым и возвращает функцию-резолвер для этого идентификатора:

ts
import { id } from 'effcss';

const badge = id({ backgroundColor: 'red' });
badge(); // 'f_5-0'
'' + badge; // 'f_5-0'

className создает единое CSS-правило с указанным содержимым и возвращает функцию, разрешающую имя класса:

ts
import { className } from 'effcss';

const btn = className({ color: 'white' });
btn(); // 'f_6-0'
'' + btn; // 'f_6-0'

Модификаторы ​

Передайте один или несколько модификаторов в функцию резолвер селекторов (id или className), чтобы создать варианты одного и того же базового правила. Для каждого варианта генерируется собственный токен, который кэшируется в привязке к набору модификаторов. Псевдоклассы и атрибуты добавляются к селектору, а at-правила (@media, @container) оборачивают правило на отдельном уровне вложенности:

ts
const box = className(() => ({ color: 'red' }));

box(); // 'f_6-0' -> `.f_6-0 { color: red; }`
box(':hover'); // 'f_6-1' -> `.f_6-1:hover { color: red; }`
box('[data-state="open"]'); // 'f_6-2' -> `.f_6-2[data-state="open"] { color: red; }`
box('@media (max-width: 700px)'); // 'f_6-3' -> `@media (max-width: 700px) { .f_6-3 { ... } }`
box('@media (max-width: 700px)', ':hover'); // 'f_6-4' -> `@media (...) { .f_6-4:hover { ... } }`

Одно и то же базовое правило используется в разных вариантах — условия применяются там, где правило используется, а не закладываются в его определение.

«Ленивые» стили и токены ​

Если ваши стили ссылаются на другие токены (резолверы), оберните их в функцию () => ({...}). Передача уже вычисленного токена в обычном объекте приведет к ошибке TypeError, поскольку это нарушает принцип «ленивого» вычисления и создает побочные эффекты на уровне модуля:

ts
const size = variable('12px');

// ❌ throws: правило содержит результат вызова резолвера,
// поэтому оно создает побочный эффект на уровне модуля
className({ padding: size() });

// ✅ lazy: токен разрешается только тогда, когда используется правило
className(() => ({ padding: size() }));

Использование функции позволяет отложить вычисление стилей: ничего не генерируется до тех пор, пока резолвер не будет вызван впервые или преобразован в строку. Это относится ко всем утилитам, принимающим объект с токенами: className, id, style и animation.

Нативная вложенность CSS ​

Любой объект стилей поддерживает вложенные селекторы, что позволяет добавлять псевдоклассы, атрибуты, дочерние элементы и комбинаторы. Символ & указывает на текущий селектор:

ts
const card = className({
    padding: '1rem',
    '&:hover': { boxShadow: '0 0 8px' }, // `.f_6-0:hover`
    '&[data-active]': { borderColor: 'blue' }, // `.f_6-0[data-active]`
    '& > .title': { fontWeight: 600 } // `.f_6-0 > .title`
});

Внутри функциональных форм можно даже передавать целые селекторы (токены) в качестве вложенных ключей — они вычисляются «лениво» (по мере необходимости) в момент использования правила:

ts
const card = className(() => ({
    padding: '1rem',
    ['& > ' + btn]: { marginTop: '1rem' } // `& > .f_6-0`
}));

variable ​

variable создает CSS-правило @property и возвращает функцию для разрешения значения переменной:

ts
import { variable } from 'effcss';

const accent = variable('#ff0');
'' + accent; // '--f_2-0'
accent(); // 'var(--f_2-0)'
accent('green'); // 'var(--f_2-0, green)'

Можно передать объект конфигурации, чтобы задать syntax, inherits и initialValue:

ts
const size = variable({ initialValue: '12px', syntax: '<length>', inherits: false });

Получить и задать начальное значение переменной можно с помощью соответствующих методов:

ts
const size = variable('12px');
size(); // материализует переменную
size.get(); // '12px'
size.set('24px'); // обновляет правило @property
size.set((prev) => prev.replace('px', 'rem')); // функциональное обновление

INFO

Методы set/get работают только после того, как переменная материализована (resolver() или resolver + ''). До этого момента get() возвращает пустую строку, а set() не имеет никакого эффекта.

animation ​

animation создает CSS-правило @keyframes и возвращает функцию для разрешения названия ключевых кадров:

ts
import { animation } from 'effcss';

const spin = animation({ from: { transform: 'rotate(0)' }, to: { transform: 'rotate(360deg)' } });
spin(); // 'f_3-0'
'' + spin; // 'f_3-0'

// функциональная форма, когда конфиг использует токены EffCSS
const grow = animation(() => ({ from: { height: size() }, to: { height: '100px' } }));
grow(); // 'f_3-1'

container ​

container формирует CSS-правило @container. При приведении к строке получается правило @container, а при вызове — конструкция name / type:

ts
import { container } from 'effcss';

const card = container('inline-size');
'' + card; // '@container f_4-0'
card(); // 'f_4-0 / inline-size'

// если тип не указан, используется значение по умолчанию
const plain = container();
'' + plain; // '@container f_4-1'
plain(); // 'f_4-1 / normal'

font ​

font создает CSS-правило @font-face и возвращает строку, пригодную для использования в коде: она содержит название шрифта (семейства) и, при необходимости, альтернативные варианты (резервные шрифты):

ts
import { font } from 'effcss';

const Inter = font({ src: "url('/inter.woff2')" });
Inter(); // 'f_1-0'
Inter('sans-serif'); // 'f_1-0, sans-serif'
Inter('Arial', 'sans-serif'); // 'f_1-0, Arial, sans-serif'

layers ​

layers создает единое правило CSS-декларации @layer, включающее все переданные слои. Функция возвращает объект, содержащий функции для разрешения слоев — по одной для каждого имени слоя:

ts
import { layers } from 'effcss';

const myLayers = layers(['reset', 'base']);
myLayers.reset(); // '@layer f_0-0'
'' + myLayers.reset; // '@layer f_0-0'
myLayers.base(); // '@layer f_0-1'

style ​

style создает произвольные стили и возвращает функцию-резолвер, которая при первом вызове внедряет их (возвращая при этом пустую строку):

ts
import { style } from 'effcss';

const custom = style({ body: { margin: 0 } });
custom(); // '' (styles are injected)
'' + custom; // ''

// передайте функцию при использовании токенов внутри стиля
const themed = style(() => ({ body: { color: accent(), ['& > ' + btn]: { marginTop: '1rem' } } }));
themed(); // ''

style поддерживает нативную вложенность CSS и специальные значения:

ts
// значения массива используются последовательно с одним ключом
style(() => ({ body: { margin: ['0', 'auto'] } }));

// массив объектов содержит несколько правил с одинаковым ключом
// (работает на любом уровне вложенности)
style({
    '@font-face': [
        { fontFamily: '"Bitstream Vera Serif Bold"', src: 'url("fira.woff2")' },
        { fontFamily: '"MyHelvetica"', src: 'local("Helvetica Neue Bold")', fontWeight: 'bold' }
    ]
});

// значение пустой строки генерирует декларацию
style(() => ({ '@layer custom': '' })); // @layer custom;

// свойства в camelCase конвертируются в kebab-case
style(() => ({ body: { fontFamily: 'serif' } }));

theme ​

theme группирует переменные и их значения по вариантам темы для упрощения динамического переключения стилей. Возвращает объект-контроллер с методами vars, list, get и set:

ts
import { theme } from 'effcss';

const myTheme = theme({
    vars: { bg: '#eff', fg: 'black' },
    options: {
        light: { bg: '#eff', fg: 'black' },
        dark: { bg: '#111', fg: 'white' }
    },
    initial: 'light'
});

myTheme.get(); // 'light'
myTheme.set('dark'); // true
myTheme.get(); // 'dark'

myTheme.vars.bg(); // 'var(--f_2-0)'
myTheme.vars.bg.get(); // '#111'
myTheme.vars.bg.set('#eee'); // переопределить одну переменную

serialize ​

serialize преобразует все созданные таблицы стилей в HTML-строку с тегами <style>:

ts
import { className, serialize } from 'effcss';

className({ color: 'white' })();

// вставьте сгенерированные теги <style> в HTML
const html = serialize();
// '<style data-effcss-key="f-6" data-effcss-count="1">.f_6-0 { color: white; }</style>'

Таким образом, стили можно подготовить на сервере и повторно использовать на клиенте. Это особенно полезно для SSG/SSR.

setConfig / getConfig ​

getConfig возвращает текущую конфигурацию глобального контекста стилей. setConfig изменяет конфигурацию и влияет на генерацию стилей, если вызывается до создания первой таблицы стилей:

ts
import { setConfig, getConfig } from 'effcss';

setConfig({ key: 'app' }); // должен быть вызван до первого использования
getConfig(); // { key: 'app' }

setConfig возвращает true, если изменения применены, или false, если провайдер уже был использован. Ключ должен начинаться с буквы и должен содержать только буквы, цифры, символы _ или -.

clear ​

clear удаляет все сгенерированные стили и сбрасывает счетчики:

ts
import { clear } from 'effcss';

clear();

context ​

context позволяет создать отдельную группу стилей, возвращая объект со всем набором утилит EffCSS, за исключением самой утилиты. У каждого контекста есть уникальный ключ, счетчики и таблицы стилей, поэтому токены в разных контекстах не пересекаются:

ts
import { context } from 'effcss';

const ctx = context({ key: 'app' });
const btn = ctx.className({ color: 'white' });
btn(); // 'app_6-0'

ctx.serialize(); // '<style data-effcss-key="app-6" data-effcss-count="1">...</style>'
ctx.getConfig(); // { key: 'app' }
ctx.clear(); // очищает стили, сохраняя контекст доступным для использования
ctx.dispose(); // безвозвратно уничтожает контекст

getContexts возвращает все активные локальные контексты, отсортированные по ключу. Контексты, которые были удалены пользовательским кодом или сборщиком мусора, не учитываются (на них указывают слабые ссылки):

ts
import { getContexts } from 'effcss';

const ctx = context({ key: 'app' });
getContexts(); // { app: ctx }

Поскольку стили context можно независимо сериализовать и восстанавливать (гидратировать), эту возможность можно использовать для изоляции стилей между микрофронтендами. Кроме того, context реализует Symbol.dispose, благодаря чему его можно использовать с оператором using.

Опубликовано под лицензией Apache License 2.0