Руководство
В этом разделе мы установим EffCSS и посмотрим, как им пользоваться.
Установка
Введите в вашем терминале:
# npm
npm i effcss
# pnpm
pnpm add effcss
# yarn
yarn add effcssТипизация CSS
По умолчанию EffCSS работает в гибком режиме: стили принимают любые объекты. Чтобы включить строгую типизацию CSS (автодополнение и проверку на недопустимые значения), один раз определите EffCSS.Properties в своем проекте, расширив глобальное пространство имен.
Например, можно установить пакет csstype
npm i -D csstypeЗатем объедините с EffCSS
// 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-правило с указанным содержимым и возвращает функцию-резолвер для этого идентификатора:
import { id } from 'effcss';
const badge = id({ backgroundColor: 'red' });
badge(); // 'f_5-0'
'' + badge; // 'f_5-0'className создает единое CSS-правило с указанным содержимым и возвращает функцию, разрешающую имя класса:
import { className } from 'effcss';
const btn = className({ color: 'white' });
btn(); // 'f_6-0'
'' + btn; // 'f_6-0'Модификаторы
Передайте один или несколько модификаторов в функцию резолвер селекторов (id или className), чтобы создать варианты одного и того же базового правила. Для каждого варианта генерируется собственный токен, который кэшируется в привязке к набору модификаторов. Псевдоклассы и атрибуты добавляются к селектору, а at-правила (@media, @container) оборачивают правило на отдельном уровне вложенности:
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, поскольку это нарушает принцип «ленивого» вычисления и создает побочные эффекты на уровне модуля:
const size = variable('12px');
// ❌ throws: правило содержит результат вызова резолвера,
// поэтому оно создает побочный эффект на уровне модуля
className({ padding: size() });
// ✅ lazy: токен разрешается только тогда, когда используется правило
className(() => ({ padding: size() }));Использование функции позволяет отложить вычисление стилей: ничего не генерируется до тех пор, пока резолвер не будет вызван впервые или преобразован в строку. Это относится ко всем утилитам, принимающим объект с токенами: className, id, style и animation.
Нативная вложенность CSS
Любой объект стилей поддерживает вложенные селекторы, что позволяет добавлять псевдоклассы, атрибуты, дочерние элементы и комбинаторы. Символ & указывает на текущий селектор:
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`
});Внутри функциональных форм можно даже передавать целые селекторы (токены) в качестве вложенных ключей — они вычисляются «лениво» (по мере необходимости) в момент использования правила:
const card = className(() => ({
padding: '1rem',
['& > ' + btn]: { marginTop: '1rem' } // `& > .f_6-0`
}));variable
variable создает CSS-правило @property и возвращает функцию для разрешения значения переменной:
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:
const size = variable({ initialValue: '12px', syntax: '<length>', inherits: false });Получить и задать начальное значение переменной можно с помощью соответствующих методов:
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 и возвращает функцию для разрешения названия ключевых кадров:
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:
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 и возвращает строку, пригодную для использования в коде: она содержит название шрифта (семейства) и, при необходимости, альтернативные варианты (резервные шрифты):
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, включающее все переданные слои. Функция возвращает объект, содержащий функции для разрешения слоев — по одной для каждого имени слоя:
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 создает произвольные стили и возвращает функцию-резолвер, которая при первом вызове внедряет их (возвращая при этом пустую строку):
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 и специальные значения:
// значения массива используются последовательно с одним ключом
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:
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>:
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 изменяет конфигурацию и влияет на генерацию стилей, если вызывается до создания первой таблицы стилей:
import { setConfig, getConfig } from 'effcss';
setConfig({ key: 'app' }); // должен быть вызван до первого использования
getConfig(); // { key: 'app' }setConfig возвращает true, если изменения применены, или false, если провайдер уже был использован. Ключ должен начинаться с буквы и должен содержать только буквы, цифры, символы _ или -.
clear
clear удаляет все сгенерированные стили и сбрасывает счетчики:
import { clear } from 'effcss';
clear();context
context позволяет создать отдельную группу стилей, возвращая объект со всем набором утилит EffCSS, за исключением самой утилиты. У каждого контекста есть уникальный ключ, счетчики и таблицы стилей, поэтому токены в разных контекстах не пересекаются:
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 возвращает все активные локальные контексты, отсортированные по ключу. Контексты, которые были удалены пользовательским кодом или сборщиком мусора, не учитываются (на них указывают слабые ссылки):
import { getContexts } from 'effcss';
const ctx = context({ key: 'app' });
getContexts(); // { app: ctx }Поскольку стили context можно независимо сериализовать и восстанавливать (гидратировать), эту возможность можно использовать для изоляции стилей между микрофронтендами. Кроме того, context реализует Symbol.dispose, благодаря чему его можно использовать с оператором using.