Skip to content

romochka/esm-styles

Repository files navigation

ESM Styles

A CSS-in-JS solution for JavaScript/TypeScript projects.

Features

  • JavaScript to CSS conversion with an intuitive object syntax
  • Build CSS from organized source files with a simple CLI
  • CSS layering support for proper style encapsulation
  • Media query and device/theme selectors with shorthands
  • CSS variable sets for different themes and devices

Installation

npm install esm-styles

Usage

Basic Concept

ESM Styles lets you write and store styles in JavaScript this way:

// article.styles.mjs
import $device from './$device.mjs'
import $theme from './$theme.mjs'
import { card } from './card.styles.mjs'

export default {
  article: {
    display: 'flex',

    card,

    '@max-tablet': {
      flexDirection: 'column',
    },

    button: {
      borderRadius: $device.radii.md,
      backgroundColor: $theme.paper.tinted,

      '@dark': {
        fontWeight: 300,
      },
    },
  },
}

Sample Directory Structure

monorepo/
├── package.json
└── packages/
  ├── app/
  │ ├── package.json
  │ └── src/
  │   ├── css/
  │   ├── styles.css
  │   └── components/
  │     ├── article.tsx
  │     ├── button.tsx
  │     └── card.tsx
  └── styles/
    ├── $device.mjs
    ├── $theme.mjs
    ├── components/
    │ ├── article.styles.mjs
    │ ├── button.styles.mjs
    │ └── card.styles.mjs
    ├── components.styles.mjs
    ├── esm-styles.config.js
    └── package.json

CLI Usage

Build your styles by creating a configuration file and running the CLI:

npx build

Or specify a custom config:

npx build path/to/config.js

Watch for changes:

npx watch

Configuration

Create a esm-styles.config.js in your project root (or use a custom path):

export default {
  basePath: './src/styles',
  sourcePath: 'source',
  outputPath: 'css',
  sourceFilesSuffix: '.styles.mjs',

  // Input floors (replaces layers)
  floors: [
    { source: 'defaults', layer: 'defaults' },
    { source: 'components', layer: 'components' },
    { source: 'layout', layer: 'layout' },
  ],

  // Specify which floors to include in main CSS
  importFloors: ['defaults', 'components', 'layout'],

  // Output
  mainCssFile: 'styles.css',

  // Global variables
  globalVariables: 'global',
  globalRootSelector: ':root',

  // Media types and queries
  media: {
    theme: ['light', 'dark'],
    device: ['mobile', 'tablet', 'desktop'],
  },

  mediaSelectors: {
    theme: {
      light: [
        {
          selector: '.light',
        },
        {
          selector: '.auto',
          mediaQuery: 'screen and (prefers-color-scheme: light)',
          prefix: 'auto',
        },
      ],
      dark: [
        {
          selector: '.dark',
        },
        {
          selector: '.auto',
          mediaQuery: 'screen and (prefers-color-scheme: dark)',
          prefix: 'auto',
        },
      ],
    },
    // Device selectors
    device: {
      mobile: [
        {
          mediaQuery: 'screen and (max-width: 767px)',
        },
      ],
      tablet: [
        {
          mediaQuery: 'screen and (min-width: 768px) and (max-width: 1024px)',
        },
      ],
      desktop: [
        {
          mediaQuery: 'screen and (min-width: 1025px)',
        },
      ],
    },
  },

  // Media query shorthands (in addition to media.device and media.theme names)
  mediaQueries: {
    'min-tablet': '(min-width: 768px)',
    'max-tablet': '(max-width: 1024px)',
    hover: '(hover: hover)',
    // ...whatever you want
  },

  // Import aliases (optional) - replace long relative paths with short prefixes
  aliases: {
    '@': '.', // resolves relative to sourcePath
  },
}

JS to CSS Translation

Basic Selectors

{
  p: {
    fontSize: '16px',
    color: 'black',

    a: {
      color: 'blue'
    },

    strong: {
      fontWeight: 'bold'
    }
  }
}

Compiles to:

p {
  font-size: 16px;
  color: black;
}

p a {
  color: blue;
}

p strong {
  font-weight: bold;
}

Class Selectors

{
  div: {
    highlighted: {
      // highlighted is not a tag
      border: '1px solid red',
    },
    p: {
      // p is a tag
      fontSize: '16px',
    },
    _video: {
      // video is a tag, but the class is meant, use single underscore prefix
      aspectRatio: 1.77,
    }
  }
}

Compiles to:

div.highlighted {
  border: 1px solid red;
}

div p {
  font-size: 16px;
}

div.video {
  aspect-ratio: 1.77;
}

Double Underscore for Descendant Class Selector

{
  modal: {
    position: 'relative',

    __close: {
      position: 'absolute',
      top: '10px',
      right: '10px'
    }
  }
}

Compiles to:

.modal {
  position: relative;
}

.modal .close {
  position: absolute;
  top: 10px;
  right: 10px;
}

Multiple Selectors

{
  'button, .btn': {
    padding: '10px 20px'
  },

  'input[type="text"], input[type="email"]': {
    borderRadius: '4px'
  }
}

Nested Media Queries

{
  card: {
    display: 'flex',

    '@media (max-width: 768px)': {
      flexDirection: 'column',

      '@media (orientation: portrait)': {
        padding: '10px'
      }
    }
  }
}

Named Media Queries

{
  main: {
    display: 'grid',

    '@mobile': {
      display: 'flex',
      flexDirection: 'column'
    },

    '@desktop': {
      gridTemplateColumns: 'repeat(3, 1fr)'
    }
  }
}

Theme Support

{
  card: {
    backgroundColor: 'white',
    color: 'black',

    '@dark': {
      backgroundColor: '#222',
      color: 'white'
    }
  }
}

CSS Variables

Define variables in a global variables file:

// global.styles.mjs
export default {
  colors: {
    primary: '#4285f4',
    secondary: '#34a853',
    error: '#ea4335',
  },
  spacing: {
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
}

Define theme-specific variables:

// light.styles.mjs
export default {
  paper: {
    bright: '#ffffff',
    tinted: '#f0f0f0',
  },
  ink: {
    bright: '#000000',
    faded: '#333333',
    accent: '#ff0000',
  },
}
// dark.styles.mjs
export default {
  paper: {
    bright: '#000000',
    tinted: '#323232',
  },
  ink: {
    bright: '#ffffff',
    faded: '#b3b3b3',
  },
}

Use with supporting modules:

// component.styles.mjs
import $theme from './$theme.mjs'

export default {
  button: {
    backgroundColor: $theme.paper.bright,
    color: $theme.ink.bright,
    padding: '10px 20px',
  },
}

Advanced Features

Layering with Floors

Organize your styles in floors for better control over specificity and output:

// Configuration example
floors: [
  { source: 'defaults', layer: 'defaults' },
  { source: 'components', layer: 'components' },
  { source: 'layout', layer: 'layout' },
  { source: 'utilities' }, // No layer wrapper
  { source: 'overrides', outputPath: 'special' }, // Custom output path
]

Each floor can:

  • Be wrapped in a CSS layer (optional)
  • Have a custom output path (optional)
  • Be included or excluded from the main CSS file via importFloors

The build process wraps floors in their respective layers and generates a main CSS file with proper import order.

CSS Variable Inheritance

Missing variables in one theme automatically inherit from the previous theme in the configuration.

Import Aliases

Simplify imports in your style files by configuring path aliases:

// esm-styles.config.js
export default {
  // ...
  aliases: {
    '@': '.',           // @ resolves to sourcePath
    '@components': './components',
  },
}

Then use in your style files:

// Before (relative paths)
import $theme from '../../$theme.mjs'
import { button } from '../components/button.styles.mjs'

// After (with aliases)
import $theme from '@/$theme.mjs'
import { button } from '@components/button.styles.mjs'

Alias paths are resolved relative to the sourcePath directory. This feature uses esbuild internally for fast module resolution.

IDE Support for Aliases

To enable Cmd+click navigation and IntelliSense for aliased imports, create a jsconfig.json in your styles source directory with matching path mappings:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Additional documentation

For humans: doc/usage-guide.md

For AI assistants: doc/ai-guide.md

API reference: doc/api-reference.md

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors