Write CSS directly in JSX component functions using EMCAScript labels.
Zero runtime overhead—styles are extracted to real stylesheets at build time.
No wrapper components, no tagged templates, just natural syntax.
Here's what Expressive JSX looks like in practice:
export const Card = ({ featured, children }) => {
// These are called labels, legal JavaScript usually for loops
// They can be repurposed to define styles within a component
background: white;
padding: 24; // Numbers convert to px units
borderRadius: 0.5; // Decimals convert to em units
boxShadow: 0xeee; // Hexidecimals convert to color
// Conditional styling based on props and/or state
if (featured) {
border: 0x007bff, 2;
}
// Labeled blocks create reusable scope, like CSS classes.
header: {
fontSize: 1.5;
fontWeight: bold;
marginBottom: 16;
}
button: {
padding: 8, 16;
borderRadius: 6;
background: 0x007bff;
color: white;
// Pseudo-selectors as string conditionals
if (':hover') {
background: 0x0056b3;
}
// scopes can be nested to apply to children or both-x-y
left: {
// $variables stand-in for CSS variables
background: $buttonLight;
}
}
return (
<div>
<h2 _header>{children}</h2>
{/* Apply scope to elements with attributes */}
<button _button _left>Learn More</button>
<button _button>Share</button>
</div>
);
};Compiles to JSX with class names:
export const Card = ({ featured, children }) => {
return (
<div className={`Card_a3f ${featured ? 'featured_x9k' : ''}`}>
<h2 className="header_b2c">{children}</h2>
<button className="button_d4e left_f1g">Learn More</button>
<button className="button_d4e">Share</button>
</div>
);
};And corresponding CSS:
.Card_a3f { background: white; padding: 24px; border-radius: 0.5em; box-shadow: #eee 0 0 10px; }
.featured_x9k { border: 2px solid #007bff; }
.header_b2c { font-size: 1.5em; font-weight: bold; margin-bottom: 16px; }
.button_d4e { padding: 8px 16px; border-radius: 6px; background: #007bff; color: white; }
.button_d4e:hover { background: #0056b3; }
.left_f1g { background: var(--button-light); }Styles live directly in your component logic with zero runtime overhead. Underscore attributes (_header, _button) apply labeled styles, conditionals use native if statements, and it's all valid upcycled JavaScript!
Expressive JSX reinterprets existing JavaScript syntax to extract CSS intent:
- JavaScript labels (you know, those things from
forloops?) become style scopesYou may have seen one before - they look like this in practice:
function example() { outer: for (let i = 0; i < 10; i++) { for (let j = 0; j < 10; j++) { console.log(i, j); if (i * j > 50) continue outer; } } }
- Bare Identifiers inside components become CSS properties
- Underscore attributes (
_label) on JSX elements apply those styles - At build time, a Babel plugin extracts this as metadata to generate stylesheets
- Components render with generated
className- zero runtime needed!
It's not a custom DSL or new syntax. It's taking JavaScript features that exist but are rarely used, and giving them new meaning at build time.
Tailwind requires memorizing utility classes and encourages class repetition:
const Button = ({ primary, children }) => (
<button className={`rounded-lg px-6 py-3 transition-all hover:brightness-110 ${
primary ? 'bg-blue-600' : 'bg-gray-600'
}`}>
{children}
</button>
);CSS Modules require context switching between files:
import styles from './Button.module.css';
const Button = ({ primary, children }) => (
<button className={`${styles.button} ${primary ? styles.primary : ''}`}>
{children}
</button>
);Styled-components add runtime overhead and wrapper components:
const Button = styled.button`
background: ${props => props.primary ? '#007bff' : '#6c757d'};
border-radius: 8px;
padding: 0.7rem 1.4rem;
`;Expressive eliminates these drawbacks: no runtime, no separate files, no memorization, no wrapper components.
|
Expressive const Card = ({ children }) => {
background: white;
borderRadius: 10;
padding: 30;
return <div>{children}</div>;
}; |
CSS Modules import styles from './Card.module.css';
const Card = ({ children }) => (
<div className={styles.card}>
{children}
</div>
);.card {
background: white;
border-radius: 10px;
padding: 30px;
} |
|
Expressive const Button = ({ disabled }) => {
cursor: pointer;
if (disabled) {
opacity: 0.4;
cursor: notAllowed;
}
return <button>Click me</button>;
}; |
CSS Modules import styles from './Button.module.css';
const Button = ({ disabled }) => (
<button
className={classNames(
styles.button,
disabled && styles.disabled
)}
>
Click me
</button>
);.button { cursor: pointer; }
.disabled { opacity: 0.4; cursor: not-allowed; } |
Note: You can use camelCase identifiers as values (like pointer and notAllowed), which are automatically converted to kebab-case (pointer → "pointer", notAllowed → "not-allowed").
export const Link = (props) => {
color: '#666';
textDecoration: 'none';
if (':hover') {
color: '#007bff';
textDecoration: 'underline';
}
if ('.active') {
fontWeight: 'bold';
}
return <a {...props} />;
};Define styles once and reuse them infinitely. This avoids Tailwind's repetitive class strings:
|
Expressive const Dashboard = () => {
display: flex;
gap: 20;
card: {
background: white;
padding: 24;
radius: 12;
shadow: 0xeee;
label: {
fontSize: 0.875;
color: 0x666;
textTransform: uppercase;
}
value: {
fontSize: 2;
fontWeight: bold;
}
}
return (
<div>
<div _card>
<div _label>Revenue</div>
<div _value>$45,231</div>
</div>
<div _card>
<div _label>Users</div>
<div _value>1,429</div>
</div>
</div>
);
}; |
Tailwind (repetitive classes) const Dashboard = () => (
<div className="flex gap-5">
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-600 uppercase">
Revenue
</div>
<div className="text-3xl font-bold">
$45,231
</div>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm">
<div className="text-sm text-gray-600 uppercase">
Users
</div>
<div className="text-3xl font-bold">
1,429
</div>
</div>
</div>
); |
Reference labels with underscore attributes (_inner) to apply styles. This creates semantic, reusable style "slots" without the verbosity of Tailwind's repeated utility classes.
Expressive ships with common macros that expand CSS patterns. All macros are user-definable so you can customize or create your own:
export const Box = () => {
absolute: fill; // → position: absolute; top: 0; right: 0; bottom: 0; left: 0;
size: 100; // → width: 100px; height: 100px;
radius: 'round'; // → border-radius: 999px;
margin: 10, 20; // → margin: 10px 20px;
marginV: 20; // → margin-top: 20px; margin-bottom: 20px;
shadow: 0xccc; // → box-shadow: #ccc 0 0 10px;
border: 0xddd, 2; // → border: 2px solid #ddd;
flexAlign: 'center'; // → display: flex; justify-content: center; align-items: center;
return <div />;
};Define custom macros in your Babel preset configuration; they're just functions which return a CSS property object.
// Custom macro example
export function elevation(level) {
const shadows = {
1: '0 1px 3px rgba(0,0,0,0.12)',
2: '0 3px 6px rgba(0,0,0,0.16)',
3: '0 10px 20px rgba(0,0,0,0.19)',
};
return { boxShadow: shadows[level] };
}
// Usage in component
const Card = () => {
elevation: 2; // → box-shadow: 0 3px 6px rgba(0,0,0,0.16)
return <div />;
};Numbers and hex colors are automatically processed:
const Component = () => {
fontSize: 1.2; // → font-size: 1.2em (decimals → em)
padding: 20; // → padding: 20px (integers → px)
color: 0x007bff; // → color: #007bff (hex numbers)
background: 0xfff8; // → background: rgba(255, 255, 255, 0.533) (hex with alpha)
width: fill; // → width: 100% (keyword, camelCase → kebab-case)
borderRadius: round; // → border-radius: 999px (camelCase identifier)
return <div />;
};Use $ prefix for theme variables (compiles to var(--kebab-case)):
const Button = () => {
// Define a CSS variable downstream
$accent: 0x007bff;
background: $accent;
color: $textPrimary;
if (':hover') {
background: $accentHover;
}
return <button>Click</button>;
};Compiles to CSS custom properties:
.Button_xyz {
--accent: #007bff;
background: var(--accent);
color: var(--text-primary);
}Choose your build tool and follow the installation steps below.
npm install @expressive/vite-plugin// vite.config.js
import jsx from '@expressive/vite-plugin';
import react from '@vitejs/plugin-react';
export default {
plugins: [
jsx(),
react()
]
};npm install @expressive/nextjs-plugin// next.config.js
const withExpressive = require('@expressive/nextjs-plugin');
module.exports = withExpressive({
// your Next.js config
});npm install @expressive/webpack-plugin// webpack.config.js
const ExpressivePlugin = require('@expressive/webpack-plugin');
module.exports = {
plugins: [
new ExpressivePlugin()
]
};Install the TypeScript plugin for IDE autocomplete:
npm install --save-dev @expressive/typescript-plugin-jsx// tsconfig.json
{
"compilerOptions": {
"plugins": [
{ "name": "@expressive/typescript-plugin-jsx" }
]
}
}- Babel:
@expressive/babel-preset - Parcel:
@expressive/parcel-transformer-jsx - Rollup:
@expressive/rollup-plugin-jsx
| Feature | Expressive | Tailwind | Styled Components | Emotion | CSS Modules | Inline Styles |
|---|---|---|---|---|---|---|
| Learning curve | Low | Medium | Medium | Medium | Low | None |
| No runtime | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| Pseudo-selectors | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Dynamic styles | ✅ | ✅ | ✅ | ✅ | ✅ | |
| No wrapper components | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| Type safety | ✅ | ❌ | ✅ | |||
| Collocated styles | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| WYSIWYG (no memorization) | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Reusable style definitions | ✅ | ✅ | ✅ | ✅ | ❌ | |
| Build-time extraction | ✅ | ✅ | ✅ | ❌ | ||
| Portable (no context switching) | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
- Documentation (coming soon)
- Examples
- GitHub
Quick reference guide to Expressive JSX concepts and terminology.
Styles defined at the top of a component function (before any labels) that automatically apply to the outermost returned element(s). No need to reference them explicitly—they just work.
const Card = () => {
// These styles apply to the root <div>
padding: 20;
background: 'white';
return <div>Content</div>;
};Named blocks that create scoped style contexts. Labels are referenced using underscore attributes (_labelName) on JSX elements.
const Component = () => {
header: {
fontSize: 24;
fontWeight: 'bold';
}
return <div _header>Title</div>;
};The syntax used to apply labeled styles to elements: <div _labelName />. The underscore prefix tells Expressive to apply that label's styles. The attribute is removed in the final output.
Functions that expand shorthand syntax into full CSS properties. All macros are library or user-defined—you have complete control to create your own design system. Expressive ships with common macros like absolute, size, radius, margin, padding, shadow, border, and flexAlign, but you can define custom ones in your Babel configuration.
// Macro input
size: 100, 200;
radius: round;
absolute: fill;
// Expands to CSS
width: 100px;
height: 200px;
border-radius: 999px;
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;To create your own macro, define a function that returns CSS property objects:
// In babel preset config
export function customMacro(arg) {
return { customProperty: processedValue };
}Internal structures that represent different scopes where styles can be defined:
- Component context: The top-level function scope
- Label context: Created by
labelName: { ... } - Conditional context: Created by
if (condition) { ... }
Using if statements to apply styles based on props or create CSS selectors:
// Prop-based conditional
if (disabled) {
opacity: 0.4;
}
// Pseudo-selector
if (':hover') {
background: 'blue';
}
// Class selector
if ('.active') {
fontWeight: 'bold';
}Automatic conversion of numeric values to CSS units:
- Integers →
px:20becomes"20px" - Decimals →
em:1.5becomes"1.5em" - Zero →
"0"(no unit needed)
You can use camelCase identifiers as values, which are automatically converted to kebab-case strings:
cursor: pointer; // → cursor: "pointer"
cursor: notAllowed; // → cursor: "not-allowed"
textDecoration: underline; // → text-decoration: "underline"
width: fill; // → width: "100%" (special keyword)
borderRadius: round; // → border-radius: "999px" (special keyword)This provides cleaner syntax without quote marks for common CSS values.
Numeric hex color notation using 0x prefix instead of #:
color: 0xff0000; // → color: #ff0000
background: 0xfff8; // → background: rgba(255, 255, 255, 0.533) (with alpha)Using $ prefix to reference CSS custom properties. CamelCase is automatically converted to kebab-case:
background: $primaryColor; // → background: var(--primary-color)
border: $accentBorder; // → border: var(--accent-border)Using comma syntax for properties that accept multiple values:
padding: 10, 20; // → padding: 10px 20px
margin: 5, 10, 15, 20; // → margin: 5px 10px 15px 20pxThe entire process happens during the build—no runtime JavaScript is needed for styling. The Babel plugin transforms labeled statements into CSS, extracts them to separate files, and replaces them with className attributes.
In Vite, CSS is injected via virtual modules (prefixed with \0virtual:css:*) that are automatically imported into your components. This enables hot module replacement (HMR) during development.
MIT