A PostCSS plugin that provides composable utilities for scalable, theme-aware styling.
— Made for scalable UI design.
Includes color mode conversion, dynamic theming, mixins, and scale-based units — AST-aware & Tailwind-friendly.
- ✅ Convert
rem(),em(), andscale()units to responsive expressions - 🎨 Parse
rgb(),hsl(),lab(),oklch(), etc. with contextualvar()resolution - 🌗 Theme switching with
themes()and@mixin dark,@mixin light, etc. - 🧩 Mixins for
hover,ltr/rtl, breakpoints, and more - ⚙️ Fully configurable via
postcss.config.js - 🎯 Tailwind-compatible with support for arbitrary values and custom variants
- 🔍 AST-aware — resolves nested and inline variables properly
Required peer dependencies:
postcsspostcss-mixinspostcss-nestedpostcss-values-parser
Install postcss-composer and required PostCSS plugins:
npm install postcss-composer postcss postcss-mixins postcss-nested postcss-values-parser -DAdd postcss-composer to your postcss.config.mjs:
export default {
plugins: ['@tailwindcss/postcss', 'autoprefixer', 'postcss-composer']
};export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
'postcss-composer': {
'themes-attr': 'class', // Change to match your theme attribute (e.g. 'data-theme', 'color-scheme')
// Optional: register custom plugins or mixins
plugins: {
'postcss-import': {},
'postcss-custom-media': { preserve: false }
},
// plugins: ['postcss-import', ['postcss-custom-media', { preserve: false }]] // Alternative syntax
mixins: {
// Register custom mixin
customHover: {
'@media (hover: hover)': {
'&:hover': {
'--color': '#f00',
'@mixin-content': {} // Use the mixin content
}
}
}
}
}
}
};Note:
themes-attr: The attribute name for theme switching (e.g.class,data-theme,color-scheme), default value is 'class'.plugins: An array of PostCSS plugins to be used for theme switching. You can us the same syntax as in thepostcss.config.mjsfile.mixins: An object with custom mixin names as keys and mixin functions as values. These mixins will be available in your CSS files.
Once configured, you can use dynamic functions like scale(), rem(), em(), and themes() in your CSS:
/* css */
:root {
--muted: #151619;
--foreground: #18191d;
--emphasis: #171717;
}
.btn {
color: var(--emphasis); /* #171717 */
border-color: hls(var(--muted) / 0.5);
/* Converts to: hsl(225deg 10% 9% / 0.5) — currently supports rgb(), hsl(), hwb(), oklch() */
background-color: themes(rgb(26 27 30), var(--foreground));
/* Resolves value based on theme context (e.g. dark/light/class) */
font-size: rem(32); /* → calc(2rem * var(--scale, 1)) */
letter-spacing: em(4); /* → calc(0.25em * var(--scale, 1)) */
}Convert contextual CSS variables (even nested or fallback!) using any culori mode:
/* css */
:root {
--brand: #1a1b1e;
}
.card {
background: hsl(var(--brand) / 0.4); /* ✅ context-aware */
border-color: oklch(var(--brand, #333) / 0.2); /* ✅ fallback supported */
color: lab(var(--brand, #111, #fff) / 0.5); /* ✅ multi-level fallback */
}Supported color formats (culori Mode):
'a98' | 'cubehelix' | 'dlab' | 'dlch' | 'hsi' | 'hsl' | 'hsv' | 'hwb' | 'itp' | 'jab' | 'jch' | 'lab' | 'lab65' | 'lch' | 'lch65' | 'lchuv' | 'lrgb' | 'luv' | 'okhsl' | 'okhsv' | 'oklab' | 'oklch' | 'p3' | 'prophoto' | 'rec2020' | 'rgb' | 'xyb' | 'xyz50' | 'xyz65' | 'yiq';postcss-composer includes helpful mixins to handle interaction state and layout direction.
hover: Uses:hoveror:activedepending on deviceltr: Styles for LTR mode onlyrtl: Styles for[dir="rtl"]light: Styles scoped to[themes-attr="light"]dark: Styles scoped to[themes-attr="dark"]
CSS variables
/* variables */
:root,
:host {
@mixin light-root {
--muted: #f0f0f0;
--foreground: #202020;
--emphasis: #000000;
/* ...others */
}
@mixin dark-root {
--muted: #1a1a1a;
--foreground: #e0e0e0;
--emphasis: #ffffff;
/* ...others */
}
}CSS class
/* css */
.btn {
font-size: rem(16);
@mixin hover {
&:not([data-loading]):not(:disabled):not([data-disabled]) {
--muted: #cccccc;
--foreground: #eeeeee;
--emphasis: #ffffff;
}
}
@mixin light {
--muted: #f0f0f0;
--foreground: #202020;
--emphasis: #000000;
}
@mixin dark {
--muted: #1a1a1a;
--foreground: #e0e0e0;
--emphasis: #ffffff;
}
@mixin ltr {
margin-left: auto;
}
@mixin rtl {
margin-right: auto;
}
@mixin max 768px {
text-align: center;
}
@mixin min 768px {
text-align: start;
}
}scale(...) function can support formats like:
/* css */
.selector {
font-size: rem(32px); /* → calc(2rem * var(--scale, 1)) */
letter-spacing: em(4px); /* → calc(0.25em * var(--scale, 1)) */
padding: scale(32px); /* → calc(2rem * 1) */
gap: scale(24px, 3); /* → calc(1.5rem * 3) */
width: scale(10rem, screen); /* → calc(10rem * var(--screen-scale, 1)) */
}Examples scale function
| Input CSS | Output CSS |
|---|---|
scale(32) |
calc(2rem * 1) |
scale(32, 3) |
calc(2rem * 3) |
scale(32, large) |
calc(2rem * var(--large-scale, 1)) |
scale(32, --large) |
calc(2rem * var(--large-scale, 1)) |
scale(32, -large--scale) |
calc(2rem * var(--large-scale, 1)) |
scale(32, large-screen) |
calc(2rem * var(--large-screen-scale, 1)) |
scale(32, --large-screen) |
calc(2rem * var(--large-screen-scale, 1)) |
scale(32, -large-screen--scale) |
calc(2rem * var (--large-screen-scale, 1)) |
Resolve theme-aware values based on attribute context:
/* css */
.card {
background: themes(#1a1b1e, var(--fg)); /* → themes(light, dark) */
/* → resolves to --fg based on .dark or [data-theme="dark"] etc. */
}Customizable via themes-attr config (e.g. class, data-theme, etc.)
Works seamlessly inside Tailwind CSS with arbitrary value support:
// file.tsx
<span className="text-[themes(#1a1b1e,#fff)] [@mixin_ltr]:mr-auto" />/* globals.css */
@import 'tailwindcss';
@custom-variant mixin-light (@mixin light);
@custom-variant mixin-dark (@mixin dark);
@custom-variant mixin-ltr (@mixin ltr);
@custom-variant mixin-rtl (@mixin rtl);
@utility color-* {
color: themes(--value([ *]));
}
@utility background-* {
background: themes(--value([ *]));
}// file.tsx
<span className="color-[#1a1b1e,#fff] mixin-ltr:mr-auto" />- 🎯
var()resolution:- Supports deeply nested values (e.g.
var(--a, var(--b, #fff))) - Context-aware: resolves from closest rule → parent → root
- Supports deeply nested values (e.g.
- 🎨 Color handling:
- Uses
culoriunder the hood — all formats supported - Auto-formats based on target function (
hsl(),rgb(),hwb(),oklch(), etc.)
- Uses
- 🧩
@mixinsupport:- The
@mixindirective is used to define a mixin, which is a reusable block of CSS code. @mixin hover: will adjust between hover and active depending on the device (@media (hover: hover)vsnone)@mixin light&@mixin dark: take thethemes-attrvalue you configured, so it's flexible and can be used with any theme.
- The
- 📌 No caching to preserve dynamicity (e.g.
:hover,@media, inline overrides)- The current approach avoids caching to allow for dynamic contexts like
:hover,:active,@media, and inline styles ([--var:...]) that can produce different results depending on the DOM state. Caching is premature and can limit user experience and developer flexibility — the first thought was to keep the plugin AST-aware and context-sensitive. So yes, keep it uncached for now.
- The current approach avoids caching to allow for dynamic contexts like
Want to help improve postcss-composer?
Check out the contribution guide
