Skip to content

gabeklein/expressive-jsx

Repository files navigation


Expressive Logo

Expressive JSX

CSS-in-JS, but it's just JSX

NPM

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.


Table of Contents


Quick Look

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!


How It Works

Expressive JSX reinterprets existing JavaScript syntax to extract CSS intent:

  • JavaScript labels (you know, those things from for loops?) become style scopes

    You 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.


Compare that to...

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.


Key Features

Self-Styling 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;
}

Conditional Styles with if Statements

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").

Pseudo-Selectors as String Conditionals

export const Link = (props) => {
  color: '#666';
  textDecoration: 'none';

  if (':hover') {
    color: '#007bff';
    textDecoration: 'underline';
  }

  if ('.active') {
    fontWeight: 'bold';
  }

  return <a {...props} />;
};

Nested Selectors with Labels

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.

Property Shorthands (Macros)

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 />;
};

Smart Value Handling

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 />;
};

CSS Variables

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);
}

Installation

Choose your build tool and follow the installation steps below.

Vite

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()
  ]
};

Next.js

npm install @expressive/nextjs-plugin
// next.config.js
const withExpressive = require('@expressive/nextjs-plugin');

module.exports = withExpressive({
  // your Next.js config
});

Webpack

npm install @expressive/webpack-plugin
// webpack.config.js
const ExpressivePlugin = require('@expressive/webpack-plugin');

module.exports = {
  plugins: [
    new ExpressivePlugin()
  ]
};

TypeScript Support

Install the TypeScript plugin for IDE autocomplete:

npm install --save-dev @expressive/typescript-plugin-jsx
// tsconfig.json
{
  "compilerOptions": {
    "plugins": [
      { "name": "@expressive/typescript-plugin-jsx" }
    ]
  }
}

Other Build Tools

  • Babel: @expressive/babel-preset
  • Parcel: @expressive/parcel-transformer-jsx
  • Rollup: @expressive/rollup-plugin-jsx

Feature Parity

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)

Learn More


Glossary

Quick reference guide to Expressive JSX concepts and terminology.

Self-Styling

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>;
};

Labels

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>;
};

Underscore Attributes

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.

Macros

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 };
}

Contexts

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) { ... }

Conditional Styling

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';
}

Auto-Unit Conversion

Automatic conversion of numeric values to CSS units:

  • Integers → px: 20 becomes "20px"
  • Decimals → em: 1.5 becomes "1.5em"
  • Zero → "0" (no unit needed)

CamelCase Value Identifiers

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.

Hex Colors

Numeric hex color notation using 0x prefix instead of #:

color: 0xff0000;        // → color: #ff0000
background: 0xfff8;     // → background: rgba(255, 255, 255, 0.533) (with alpha)

CSS Variables

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)

Multi-Value Properties

Using comma syntax for properties that accept multiple values:

padding: 10, 20;              // → padding: 10px 20px
margin: 5, 10, 15, 20;        // → margin: 5px 10px 15px 20px

Build-Time Transformation

The 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.

Virtual CSS Modules

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.



License

MIT

About

JSX with first class styles

Resources

License

Stars

Watchers

Forks