npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

esm-styles

v0.4.1

Published

A library for working with ESM styles

Readme

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