0.3.1 • Published 5 months ago

@poupe/css v0.3.1

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

@poupe/css

jsDocs.io npm version License: MIT

A TypeScript utility library for CSS property manipulation, formatting, and CSS-in-JS operations.

Table of Contents

Features

  • 🛠️ Utilities for manipulating CSS properties
  • 🔄 Bidirectional conversion between camelCase and kebab-case CSS properties
  • 📝 Format CSS for use in JavaScript
  • 🎨 CSS-in-JS helpers and type definitions
  • 📦 Lightweight, tree-shakable API
  • 🧩 Support for nested CSS rules and at-rules
  • ⚡ Memory-efficient generators for large CSS files

Installation

npm install -D @poupe/css
yarn add -D @poupe/css
pnpm add -D @poupe/css

API Reference

The library exports several categories of utilities:

  • Case Conversion: Functions for converting between different CSS naming conventions
  • CSS Stringification: Tools to convert CSS objects to strings
  • CSS Variables: Utilities for working with CSS custom properties
  • CSS Rules: Functions for handling nested CSS rule objects
  • Type Definitions: TypeScript types for CSS properties

Types

CSSValue

Represents a valid CSS property value:

type CSSValue = string | number | (string | number)[];

CSSProperties<K extends string = string>

A typed record of CSS properties:

type CSSProperties<K extends string = string> = Record<K, CSSValue>;

CSSPropertiesOptions

Configuration options for CSS properties stringification:

type CSSPropertiesOptions = {
  /** Indentation string, defaults to two spaces. */
  indent?: string
  /** Prefix string added before each line, defaults to empty string. */
  prefix?: string
  /** New line character, defaults to LF. */
  newLine?: string
  /** Whether to format output on a single line, defaults to false. */
  inline?: boolean
  /** Max properties to format on a single line, defaults to 1. */
  singleLineThreshold?: number
}

CSSRules

Represents a structured CSS rule set that can contain nested rules:

type CSSRules = {
  [name: string]: null | string | string[] | CSSRules | CSSRules[]
};

CSSRuleObject

A more restrictive subset of CSSRules that is compatible with TailwindCSS plugin API:

type CSSRuleObject = {
  [key: string]: string | string[] | CSSRuleObject
};

CSSRulesFormatOptions

Configuration options for formatting CSS rules:

interface CSSRulesFormatOptions {
  /** Indentation string for each level of nesting, defaults to two spaces. */
  indent?: string
  /** Prefix string added before each line, defaults to empty string. */
  prefix?: string
  /** Optional validation function to determine which rules to include. */
  valid?: (key: string, value: CSSRulesValue) => boolean
  /** Whether to normalize CSS property names from camelCase to kebab-case. */
  normalizeProperties?: boolean
}

CSS Property Functions

stringifyCSSProperties<K extends string>

(object: CSSProperties<K>, options?: CSSPropertiesOptions): string Converts a CSSProperties object into a formatted CSS string representation with proper indentation. Property values are intelligently formatted based on their type and the CSS property name - space-delimited properties (like margin, padding) use spaces between multiple values, while other properties use commas.

import { stringifyCSSProperties } from '@poupe/css';

const styles = {
  fontSize: '16px',
  backgroundColor: 'red',
  margin: [10, '20px', '30px', '40px'],
  fontFamily: ['Arial', 'sans-serif']
};

// Default multi-line formatting
const cssString = stringifyCSSProperties(styles);
// "{
//   font-size: 16px;
//   background-color: red;
//   margin: 10 20px 30px 40px;
//   font-family: Arial, sans-serif;
// }"

// Inline formatting
const inlineCSS = stringifyCSSProperties(styles, { inline: true });
// "{ font-size: 16px; background-color: red; margin: 10 20px 30px 40px;
//   font-family: Arial, sans-serif }"

formatCSSProperties<K extends string>(object: CSSProperties<K>): string[]

Formats a CSS properties object into an array of CSS property strings. Automatically handles deduplication of properties, where later declarations override earlier ones while preserving the original insertion order.

import { formatCSSProperties } from '@poupe/css';

const styles = {
  fontSize: '16px',
  backgroundColor: 'red',
  margin: [10, '20px', '30px', '40px'],
  // Later declarations override earlier ones:
  backgroundColor: 'blue'
};

const cssLines = formatCSSProperties(styles);
// [
//   "font-size: 16px",
//   "background-color: blue", // Note: red was overridden by blue
//   "margin: 10, 20px, 30px"
// ]

formatCSSValue(value: CSSValue, useComma = true): string

Formats a CSS value into a string. If the value is an array:

  • By default, it joins the elements with commas (appropriate for properties like font-family)
  • When useComma is set to false, it uses spaces (appropriate for properties like margin or padding)

The function automatically handles quoting strings that contain spaces (except CSS functions like rgb() or calc()), enclosing them in double quotes.

import { formatCSSValue } from '@poupe/css';

formatCSSValue('16px'); // "16px"
formatCSSValue('Open Sans'); // "\"Open Sans\""
formatCSSValue([10, '20px', '30px']); // "10, 20px, 30px" (comma-separated)
formatCSSValue([10, '20px', '30px'], false); // "10 20px 30px" (space-separated)
formatCSSValue('rgb(255, 0, 0)'); // "rgb(255, 0, 0)" - Not quoted despite
formatCSSValue('calc(100% - 20px)'); // "calc(100% - 20px)" - Not quoted

properties<K extends string>

(object: CSSProperties<K>): Generator<[K, CSSValue]> Generates a sequence of valid CSS property key-value pairs from a CSSProperties object. Filters out invalid or empty CSS property values.

import { properties } from '@poupe/css';

const styles = {
  fontSize: '16px',
  backgroundColor: 'red',
  _private: 'hidden',
  empty: ''
};

// Iterate through valid properties only
for (const [key, value] of properties(styles)) {
  console.log(`${key}: ${value}`);
}
// Output:
// fontSize: 16px
// backgroundColor: red

CSS Rules Functions

stringifyCSSRules(rules: CSSRules | CSSRuleObject, options?): string

Converts a CSS rule object into a formatted string representation with proper indentation and nesting.

import { stringifyCSSRules } from '@poupe/css';

const rules = {
  'body': {
    'color': 'red',
    'font-size': '16px',
    '@media (max-width: 768px)': {
      'font-size': '14px'
    }
  },
  '.container': {
    'max-width': '1200px',
    'margin': ['0', 'auto']
  }
};

const cssString = stringifyCSSRules(rules);
// Output:
// body {
//   color: red;
//   font-size: 16px;
//   @media (max-width: 768px) {
//     font-size: 14px;
//   }
// }
// .container {
//   max-width: 1200px;
//   margin: 0 auto;
// }

formatCSSRules(rules: CSSRules | CSSRuleObject, options?): string[]

Processes a CSS rule object and returns an array of strings, where each string represents a line in the formatted CSS output. Internally uses the generateCSSRules generator for memory efficiency.

import { formatCSSRules } from '@poupe/css';

const rules = {
  'body': {
    'color': 'red',
    'font-size': '16px'
  }
};

const lines = formatCSSRules(rules);
// Returns: ['body {', '  color: red;', '  font-size: 16px;', '}']

// Custom indentation
const indentedLines = formatCSSRules(rules, { indent: '    ' });
// Returns: ['body {', '    color: red;', '    font-size: 16px;', '}']

// Property name normalization (camelCase to kebab-case)
const camelRules = {
  body: {
    fontSize: '16px',
    backgroundColor: 'blue',
    marginTop: '20px'
  }
};

const normalized = formatCSSRules(camelRules, { normalizeProperties: true });
// Returns: ['body {', '  font-size: 16px;', '  background-color: blue;', '  margin-top: 20px;', '}']

// Selectors and at-rules are intelligently preserved
const complexRules = {
  fontSize: '18px',  // Will be normalized to font-size
  '.button': {       // Selector preserved as-is
    paddingLeft: '10px'  // Will be normalized to padding-left
  },
  '@media print': {  // At-rule preserved as-is
    backgroundColor: 'white'  // Will be normalized to background-color
  }
};

const result = formatCSSRules(complexRules, { normalizeProperties: true });
// Returns:
// ['font-size: 18px;', '.button {', '  padding-left: 10px;', '}', '@media print {', '  background-color: white;', '}']

generateCSSRules(rules: CSSRules | CSSRuleObject, options?): Generator<string>

Generator version of formatCSSRules that yields lines as they're generated. More memory-efficient for large CSS files as it doesn't build the entire array in memory.

import { generateCSSRules } from '@poupe/css';

const rules = {
  'body': {
    'color': 'red',
    'font-size': '16px'
  }
};

// Use generator for streaming or large files
for (const line of generateCSSRules(rules)) {
  console.log(line);
}
// Output:
// body {
//   color: red;
//   font-size: 16px;
// }

// Or collect all lines (same as formatCSSRules)
const lines = [...generateCSSRules(rules)];

formatCSSRulesArray

(rules: (string | CSSRules | CSSRuleObject)[], options?): string[] Formats an array of CSS rules into indented lines recursively. Internally uses the generateCSSRulesArray generator for memory efficiency.

import { formatCSSRulesArray } from '@poupe/css';

const rulesArray = [
  { 'color': 'red' },
  { 'font-size': '16px' },
  'font-weight: bold',
  {
    '@media (max-width: 768px)': {
      'font-size': '14px'
    }
  }
];

const lines = formatCSSRulesArray(rulesArray);
// Returns lines with proper indentation for each rule

generateCSSRulesArray

(rules: (string | CSSRules | CSSRuleObject)[], options?): Generator<string> Generator version of formatCSSRulesArray that yields lines as they're generated. Efficiently handles large arrays of CSS rules without building the entire result in memory.

import { generateCSSRulesArray } from '@poupe/css';

const rulesArray = [
  { 'color': 'red' },
  '',  // Empty string creates blank line
  { 'font-size': '16px' }
];

// Stream through large rule arrays
for (const line of generateCSSRulesArray(rulesArray)) {
  process.stdout.write(line + '\n');
}

defaultValidCSSRule(key: string, value: CSSRulesValue): boolean

Default validation function that determines if a CSS rule key-value pair should be included in the output. A rule is considered valid if the key is not empty and the value is neither undefined nor null.

import { defaultValidCSSRule } from '@poupe/css';

// Use with custom rule validation
const customValid = (key, value) => {
  return defaultValidCSSRule(key, value) && !key.startsWith('_');
};

interleavedRules(rules: CSSRules[]): CSSRules[]

Interleaves an array of CSS rule objects with empty objects, useful for creating spacing between rule blocks in the output.

import { interleavedRules } from '@poupe/css';

const rules = [
  { '.button': { color: 'blue' } },
  { '.input': { border: '1px solid gray' } }
];

const spacedRules = interleavedRules(rules);
// Returns:
// [
//   { '.button': { color: 'blue' } },
//   {}, // Empty object for spacing
//   { '.input': { border: '1px solid gray' } }
// ]

// When stringified, this creates an empty line between rule blocks

renameRules(rules: CSSRules, fn: (name: string) => string): CSSRules

Renames the keys in a CSS rules object using the provided function, allowing for advanced selector manipulation.

import { renameRules } from '@poupe/css';

const rules = {
  '.button': { color: 'blue' },
  '.input': { border: '1px solid gray' }
};

// Add a prefix to all selectors
const prefixedRules = renameRules(rules, key => `.prefix ${key}`);
// Returns:
// {
//   '.prefix .button': { color: 'blue' },
//   '.prefix .input': { border: '1px solid gray' }
// }

// Transform selectors to utility classes
const utilityRules = renameRules(rules, key => `@utility ${key.slice(1)}`);
// Returns:
// {
//   '@utility button': { color: 'blue' },
//   '@utility input': { border: '1px solid gray' }
// }

// Return falsy from the function to skip/remove a rule
const filteredRules = renameRules(rules, key =>
  key.includes('button') ? key : null);
// Returns:
// {
//   '.button': { color: 'blue' }
// }

setDeepRule(target, path, object)

Sets a CSS rule object at a specified path within a target object, merging with existing objects and creating intermediate objects as needed. This function is overloaded to provide type safety for both general CSSRules objects and TailwindCSS-compatible CSSRuleObject types.

import { setDeepRule } from '@poupe/css';

const rules = {};

// Direct assignment
setDeepRule(rules, 'button', { color: 'blue' });
// Result: { button: { color: 'blue' } }

// Nested assignment
setDeepRule(rules, ['components', 'button'], { color: 'blue' });
// Result: { components: { button: { color: 'blue' } } }

// Merging with existing object (new values take precedence)
const existingRules = { button: { color: 'red', margin: '5px' } };
setDeepRule(existingRules, 'button', { color: 'blue', padding: '10px' });
// Result: { button: { color: 'blue', margin: '5px', padding: '10px' } }

getDeepRule(target, path)

Retrieves a CSS rule value from a specified path within a target object. This function is overloaded to provide type safety for both general CSSRules objects and TailwindCSS-compatible CSSRuleObject types.

import { getDeepRule } from '@poupe/css';

const rules = {
  components: { button: { color: 'blue' } },
  utils: ['clearfix', 'sr-only']
};

// Direct access
getDeepRule(rules, 'utils');
// Result: ['clearfix', 'sr-only']

// Nested access
getDeepRule(rules, ['components', 'button', 'color']);
// Result: 'blue'

// Non-existent path
getDeepRule(rules, ['components', 'header']);
// Result: undefined

// Root access (empty array)
getDeepRule(rules, []);
// Result: { components: { ... }, utils: [...] }

CSS Selector Functions

expandSelectorAlias(selector: string, aliases?: Record<string, string>): string

Expands selector aliases into their full forms using built-in or custom aliases.

Built-in aliases include:

  • 'media''@media (prefers-color-scheme: dark)'
  • 'dark''@media (prefers-color-scheme: dark)'
  • 'light''@media (prefers-color-scheme: light)'
  • 'mobile''@media (max-width: 768px)'
  • 'tablet''@media (min-width: 769px) and (max-width: 1024px)'
  • 'desktop''@media (min-width: 1025px)'
import { expandSelectorAlias } from '@poupe/css';

// Using built-in aliases
expandSelectorAlias('media'); // '@media (prefers-color-scheme: dark)'
expandSelectorAlias('mobile'); // '@media (max-width: 768px)'

// Using custom aliases
const customAliases = {
  'print': '@media print',
  'landscape': '@media (orientation: landscape)'
};
expandSelectorAlias('print', customAliases); // '@media print'

// Non-aliased selectors pass through unchanged
expandSelectorAlias('.my-class'); // '.my-class'

processCSSSelectors(selectors: string | string[], options?: ProcessCSSSelectorOptions): string[] | undefined

Processes CSS selectors and at-rules, handling both strings and arrays. Merges consecutive selectors with OR and adds * variants, while keeping at-rules stacked separately. Returns undefined if no valid selectors are found.

import { processCSSSelectors } from '@poupe/css';

// Single string selector
processCSSSelectors('.test');
// Result: ['.test, .test *']

// Array of selectors
processCSSSelectors(['.dark', '.custom']);
// Result: ['.dark, .dark *, .custom, .custom *']

// Comma-separated selectors pass through (when allowCommaPassthrough is true)
processCSSSelectors('.test, .other');
// Result: ['.test, .other']

// Disable star variants
processCSSSelectors(['.test1', '.test2'], { addStarVariants: false });
// Result: ['.test1, .test2']

// Use custom aliases
const customAliases = { 'custom': '@media (min-width: 1200px)' };
processCSSSelectors('custom', { aliases: customAliases });
// Result: ['@media (min-width: 1200px), @media (min-width: 1200px) *']

// Mixed selectors and aliases
processCSSSelectors(['.test', 'mobile'], { addStarVariants: false });
// Result: ['.test', '@media (max-width: 768px)']

// At-rules are kept separate
processCSSSelectors([
  '.dark',
  '@media (max-width: 768px)',
  '.mobile'
]);
// Result: [
//   '.dark, .dark *',
//   '@media (max-width: 768px)',
//   '.mobile, .mobile *'
// ]

// Returns undefined for empty arrays
processCSSSelectors([]); // undefined

// Alias expansion with single string
processCSSSelectors('media');
// Result: ['@media (prefers-color-scheme: dark), @media (prefers-color-scheme: dark) *']

// Disable comma pass-through
processCSSSelectors('.test, .other', { allowCommaPassthrough: false });
// Result: ['.test, .other, .test, .other *']

Utility Functions

unsafeKeys<T>(object: T): Array<keyof T>

A type-safe wrapper around Object.keys for preserving the object's key types.

keys<T, K extends keyof T>

(object: T, valid?: (key: keyof T) => boolean): Generator<K>

A generator function that yields keys of an object that pass an optional validation function.

import { keys } from '@poupe/css';

const obj = { a: 1, b: 2, _private: 3 };

// Use with default validation (includes all keys)
for (const key of keys(obj)) {
  console.log(key); // "a", "b", "_private"
}

// Use with custom validation
for (const key of keys(obj, k => !k.startsWith('_'))) {
  console.log(key); // "a", "b"
}

pairs<K extends string, T>

(object: Record<K, T>, valid?: (k: K, v: T) => boolean): Generator<[K, T]>

A generator function that yields valid key-value pairs from an object. Allows providing a custom validation function to determine which pairs to include.

import { pairs } from '@poupe/css';

const obj = { color: 'red', fontSize: '16px', _private: 'hidden', empty: '' };

// Use with default validation (excludes keys starting with underscore and
// null/empty values)
for (const [key, value] of pairs(obj)) {
  console.log(`${key}: ${value}`); // "color: red", "fontSize: 16px"
}

// Use with custom validation
const customValid = (key: string, value: unknown) =>
  typeof value === 'string' && value.length > 3;
for (const [key, value] of pairs(obj, customValid)) {
  console.log(`${key}: ${value}`); // "color: red", "_private: hidden"
}

defaultValidPair<K extends string, T>(key: K, value: T): boolean

Validates if a key-value pair meets default criteria:

  • The value is neither null nor undefined
  • The key doesn't contain spaces
  • The key doesn't start with an underscore (_)

kebabCase(s: string): string

Converts a given string to kebab-case:

  • Transforms camelCase, PascalCase, and snake_case to kebab-case
  • Adds leading hyphen to recognized vendor prefixes
import { kebabCase } from '@poupe/css';

kebabCase('XMLHttpRequest'); // 'xml-http-request'
kebabCase('camelCase');      // 'camel-case'
kebabCase('snake_case');     // 'snake-case'
kebabCase('WebkitTransition'); // '-webkit-transition'

camelCase(s: string): string

Converts a given string to camelCase:

  • Transforms kebab-case, PascalCase, and snake_case to camelCase
  • Properly handles vendor prefixes by removing the leading hyphen
  • Correctly handles acronyms and preserves internal capitalization
import { camelCase } from '@poupe/css';

camelCase('kebab-case');      // 'kebabCase'
camelCase('PascalCase');      // 'pascalCase'
camelCase('snake_case');      // 'snakeCase'
camelCase('-webkit-transition'); // 'webkitTransition'
camelCase('HTMLElement');     // 'htmlElement'
camelCase('BGColor');         // 'bgColor'

Usage Examples

Case Conversion Example

import { kebabCase, camelCase } from '@poupe/css';

// Kebab-case to camelCase
const camelProperty = camelCase('background-color'); // 'backgroundColor'

// CamelCase to kebab-case
const kebabProperty = kebabCase('backgroundColor'); // 'background-color'

// Useful for converting between CSS and JavaScript property names
const styleObject = {
  backgroundColor: 'red',
  fontSize: '16px'
};

// Convert to CSS properties
const cssProperties = Object.entries(styleObject).map(
  ([key, value]) => `${kebabCase(key)}: ${value};`
);
// ['background-color: red;', 'font-size: 16px;']

CSS Selector Processing Example

import { processCSSSelectors, expandSelectorAlias } from '@poupe/css';

// Theme-aware selector processing
const darkModeSelectors = processCSSSelectors(['.dark', 'media']);
// Result: ['.dark, .dark *', '@media (prefers-color-scheme: dark)']

// Custom aliases for responsive design
const customAliases = {
  'wide': '@media (min-width: 1440px)',
  'touch': '@media (hover: none) and (pointer: coarse)'
};

const responsiveSelectors = processCSSSelectors(
  ['mobile', 'wide'],
  { aliases: customAliases }
);
// Result: ['@media (max-width: 768px)', '@media (min-width: 1440px)']

// Expand individual aliases
expandSelectorAlias('tablet');
// '@media (min-width: 769px) and (max-width: 1024px)'

Integration with Poupe Ecosystem

Requirements

  • Node.js >=20.19.1
  • TypeScript-friendly environment

License

MIT licensed.

0.3.1

5 months ago

0.3.0

5 months ago

0.2.5

5 months ago

0.2.4

5 months ago

0.2.3

5 months ago

0.2.2

5 months ago

0.2.1

5 months ago

0.2.0

5 months ago

0.1.1

6 months ago

0.1.0

6 months ago