diff --git a/.changeset/small-guests-film.md b/.changeset/small-guests-film.md new file mode 100644 index 000000000..260d5fd58 --- /dev/null +++ b/.changeset/small-guests-film.md @@ -0,0 +1,5 @@ +--- +'@vanilla-extract/css': minor +--- + +Add `composeStyles` function diff --git a/README.md b/README.md index 974e91658..c6551f75b 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ document.write(` - [globalFontFace](#globalfontface) - [keyframes](#keyframes) - [globalKeyframes](#globalkeyframes) + - [composeStyles](#composestyles) - [Dynamic API](#dynamic-api) - [createInlineTheme](#createinlinetheme) - [setElementTheme](#setelementtheme) @@ -580,6 +581,28 @@ export const animated = style({ }); ``` +### composeStyles + +Combines mutliple styles into a single class string, while also deduplicating and removing unnecessary spaces. + +```ts +import { style, composeStyles } from '@vanilla-extract/css'; + +const base = style({ + padding: 12 +}); + +export const blue = composeStyles(base, style({ + background: 'blue' +})); + +export const green = composeStyles(base, style({ + background: 'green' +})); +``` + +> 💡 Styles can also be provided in shallow and deeply nested arrays. Think of it as a static version of [classnames.](https://github.com/JedWatson/classnames) + ## Dynamic API We also provide a lightweight standalone package to support dynamic runtime theming. diff --git a/packages/css/src/composeStyles.test.ts b/packages/css/src/composeStyles.test.ts new file mode 100644 index 000000000..288225fcd --- /dev/null +++ b/packages/css/src/composeStyles.test.ts @@ -0,0 +1,25 @@ +import { composeStyles } from './composeStyles'; + +describe('composeStyles', () => { + it.each([ + { args: ['1'], output: '1' }, + { args: ['1 1'], output: '1' }, + { args: ['1 2 3'], output: '1 2 3' }, + { args: ['1', '2', '3'], output: '1 2 3' }, + { args: [['1', '2'], '3'], output: '1 2 3' }, + { args: [['1', ['2', '3']], '4'], output: '1 2 3 4' }, + { + args: [['1', ['2', ['3', '4']]], ['5', '6'], '7'], + output: '1 2 3 4 5 6 7', + }, + { args: ['1', '1', '1'], output: '1' }, + { + args: ['1', ['1', '2'], ['1', '2', '3'], ['1', '2', '3', '4']], + output: '1 2 3 4', + }, + { args: ['1 2 3', '2 3 4', '1 5'], output: '1 2 3 4 5' }, + { args: [' 1 2 3 2 ', ' 2 3 4 2 ', ' 1 5 1 '], output: '1 2 3 4 5' }, + ])('composeStyles', ({ args, output }) => { + expect(composeStyles(...args)).toBe(output); + }); +}); diff --git a/packages/css/src/composeStyles.ts b/packages/css/src/composeStyles.ts new file mode 100644 index 000000000..15dd8ffbf --- /dev/null +++ b/packages/css/src/composeStyles.ts @@ -0,0 +1,30 @@ +type ClassNames = string | Array; + +function composeStylesIntoSet( + set: Set, + ...classNames: Array +) { + for (const className of classNames) { + if (className.length === 0) { + continue; + } + + if (typeof className === 'string') { + if (className.includes(' ')) { + composeStylesIntoSet(set, ...className.trim().split(' ')); + } else { + set.add(className); + } + } else if (Array.isArray(className)) { + composeStylesIntoSet(set, ...className); + } + } +} + +export function composeStyles(...classNames: Array) { + const set: Set = new Set(); + + composeStylesIntoSet(set, ...classNames); + + return Array.from(set).join(' '); +} diff --git a/packages/css/src/index.ts b/packages/css/src/index.ts index 20ab3eeb4..dbcd98736 100644 --- a/packages/css/src/index.ts +++ b/packages/css/src/index.ts @@ -5,3 +5,4 @@ export * from './identifier'; export * from './theme'; export * from './style'; export * from './vars'; +export { composeStyles } from './composeStyles';