A build-time CSS-in-JSX solution that brings the power of modern CSS to your React components with zero runtime overhead.

Note: Flair is still in early development. Expect minor bugs and issues. Feedback is welcome!
Try it online on StackBlitz.
React Vite | SolidJS Vite | Preact Vite
- 🚀 Zero Runtime - All CSS processing happens at build time
- 💅 Multiple Styling APIs - Choose between
<Style>tags,flair()objects, orcsstemplate literals - 🌙 Theme System - Built-in theming with TypeScript intellisense
- 🔧 Build Tool Integration - Supports Vite, NextJS, Rollup, Webpack, and Parcel
- 🎯 Scoped by Default - Component-scoped styles with global override option
- ⚡ Rust-Powered - Fast AST parsing with OXC and CSS processing with Lightning CSS
- 🔍 Static Analysis - Class names and CSS are analyzed at build time for optimal performance
# Install client package
npm install @flairjs/client
# Install your bundler plugin
npm install @flairjs/vite-plugin # For Vite
npm install @flairjs/rollup-plugin # For Rollup
npm install @flairjs/webpack-loader # For Webpack
npm install @flairjs/parcel-transformer # For Parcel// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import flairjs from "@flairjs/vite-plugin";
export default defineConfig({
plugins: [react(), flairjs()],
});import { flair } from "@flairjs/client";
const Button = () => {
return <button className="button">Click me!</button>;
};
// Style with flair object
Button.flair = flair({
".button": {
backgroundColor: "blue",
color: "white",
padding: "12px 24px",
borderRadius: "8px",
border: "none",
"&:hover": {
backgroundColor: "darkblue",
},
},
});
export default Button;Flair provides three ways to write CSS in your components:
import { flair } from "@flairjs/client";
const Card = () => <div className="card">Content</div>;
Card.flair = flair({
".card": {
backgroundColor: "white",
borderRadius: "8px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
padding: "16px",
},
});import { css } from "@flairjs/client";
const Card = () => <div className="card">Content</div>;
Card.flair = css`
.card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 16px;
}
`;import { Style } from "@flairjs/client/react";
const Card = () => {
return (
<>
<div className="card">Content</div>
<Style>{`
.card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 16px;
}
`}</Style>
</>
);
};By default, styles are scoped to components. You can make styles global:
import { Style } from "@flairjs/client/react";
const App = () => {
return (
<>
<Style global>{`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
`}</Style>
{/* Your app content */}
</>
);
};const App = () => <div>App content</div>;
App.globalFlair = css`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
`;To enable theming support, you need to:
- Import the theme CSS in your top-level file (e.g.,
main.tsx,App.tsx, orindex.tsx):
import "@flairjs/client/theme.css";- Create a theme configuration file
flair.theme.tsin your project root:
// flair.theme.ts
import { defineConfig } from "@flairjs/client";
const theme = defineConfig({
prefix: "flair",
selector: "body",
tokens: {
colors: {
primary: "#3b82f6",
secondary: "#64748b",
success: "#10b981",
danger: "#ef4444",
},
fonts: {
family: "'Inter', sans-serif",
size: {
sm: "14px",
md: "16px",
lg: "18px",
},
},
space: {
1: "4px",
2: "8px",
3: "12px",
4: "16px",
5: "20px",
6: "24px",
},
},
breakpoints: {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
},
});
export default theme;
export type Theme = typeof theme;import { flair } from "@flairjs/client";
const Button = () => <button className="button">Click me</button>;
Button.flair = flair({
".button": {
backgroundColor: "$colors.primary",
color: "white",
padding: "$space.3 $space.5",
fontSize: "$fonts.size.md",
fontFamily: "$fonts.family",
},
});For theme token autocomplete, extend the FlairTheme interface:
// types/flair.d.ts
import { Theme } from "../flair.theme";
declare module "@flairjs/client" {
export interface FlairTheme extends Theme {}
}Button.flair = flair({
".button": {
padding: "$space.2 $space.3",
fontSize: "$fonts.size.sm",
// Responsive breakpoints
"$screen md": {
padding: "$space.3 $space.5",
fontSize: "$fonts.size.md",
},
"$screen lg": {
padding: "$space.4 $space.6",
fontSize: "$fonts.size.lg",
},
},
});// vite.config.js
import flairjs from "@flairjs/vite-plugin";
export default {
plugins: [
flairjs({
classNameList: ["className", "class"],
// Optional: Include/exclude files
include: ["src/**/*.{tsx,jsx}"],
exclude: ["node_modules/**"],
}),
],
};// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(tsx|jsx)$/,
use: ["@flairjs/webpack-loader"],
},
],
},
};// rollup.config.js
import flairjs from "@flairjs/rollup-plugin";
export default {
plugins: [
// Make sure rollup is configured to handle css imports.
// Add flair before any other JSX parsers
flairjs(),
],
};// .parcelrc
{
"extends": "@parcel/config-default",
"transformers": {
"*.{tsx,jsx}": ["@flairjs/parcel-transformer", "..."]
}
}Since Flair is a build-time library, all CSS and class names must be statically inferrable at build time. Flair cannot process dynamically generated CSS or class names that are only known at runtime.
In most cases, Flair can infer class names automatically:
// ✅ Direct className
<button className="btn">Click me</button>
// ✅ Variable stored className
const buttonClass = "btn btn-primary"
<button className={buttonClass}>Click me</button>
// ✅ Function parameters
const variant = "primary"
<button className={clsx(variant)}>Click me</button>// ❌ Function calls - Flair cannot infer the return value
<button className={someFunction()}>Click me</button>
// ❌ Complex runtime expressions
<button className={`btn btn-${variant}`}>Click me</button>When Flair cannot directly infer a class name (e.g., when returned from a function), use the c() or cn() utilities to signal which class names should be included:
import { c, cn } from "@flairjs/client";
// Both c() and cn() are identical - they simply return what you pass to them
// Their purpose is to signal to Flair's build-time analyzer which class names to include
function getButtonClass() {
// ✅ Signal to Flair that 'btn' and 'btn-primary' should be included
return c("btn btn-primary");
}
const Button = () => {
return <button className={getButtonClass()}>Click me</button>;
};
Button.flair = flair({
".btn": { padding: "12px 24px" },
".btn-primary": { backgroundColor: "blue" },
});Important Notes:
c()andcn()are not likeclsxorclassnames- they don't merge or conditionally apply classes- They are simple pass-through functions:
c('foo')just returns'foo' - Their only purpose is to help Flair's static analyzer find class names in your code
- At runtime, they have zero overhead (they literally just return their input)
Card.flair = flair({
".card": {
backgroundColor: "white",
"&:hover": {
backgroundColor: "#f9f9f9",
},
"&.active": {
borderColor: "$colors.primary",
},
"& .title": {
fontSize: "$fonts.size.lg",
fontWeight: "bold",
},
},
});Card.flair = flair({
".card": {
padding: "$space.3",
"@media (min-width: 768px)": {
padding: "$space.5",
},
},
});Currently, flair property is supported in all JSX frameworks.
Flair component is supported in:
- ✅ React (via
@flairjs/client/react) - ✅ Preact (via
@flairjs/client/preact) - ✅ SolidJS (via
@flairjs/client/solidjs)
- Zero Runtime Overhead - All CSS is extracted at build time
- Optimal Bundle Size - Only the CSS you use is included
- Fast Builds - Rust-powered transformation with OXC and Lightning CSS
- Efficient Caching - Smart caching of transformed components
Flair generates modern CSS that works in all evergreen browsers. Legacy browser support depends on your build setup and CSS processing pipeline.
We welcome contributions! Here's how to get started:
-
Clone the repository
git clone https://github.com/akzhy/flairjs.git cd flairjs -
Install dependencies
pnpm install
-
Build packages
# Build all packages pnpm build # Or build specific packages pnpm build:core # Build core Rust package pnpm build:non-core-packages # Build all other packages
When contributing changes, please follow these steps:
-
Create a new branch for your feature or bugfix
git checkout -b feature/your-feature-name
-
Make your changes and ensure all packages build successfully
-
Add a changeset to document your changes
pnpm changeset
This will prompt you to:
- Select which packages are affected by your changes
- Specify the type of change (major, minor, patch)
- Write a description of your changes
The changeset system ensures proper versioning and generates changelogs automatically.
-
Commit your changes including the changeset file
git add . git commit -m "a sensible commit message"
-
Push your branch and create a pull request
git push origin feature/your-feature-name
- Patch (0.0.x): Bug fixes, documentation updates, internal refactors
- Minor (0.x.0): New features, non-breaking enhancements
- Major (x.0.0): Breaking changes, API changes
Example changeset workflow:
# After making changes to @flairjs/vite-plugin
pnpm changeset
# You'll be prompted:
# - Select @flairjs/vite-plugin
# - Choose "patch" for a bugfix
# - Describe: "Fixed issue with theme token resolution"Before submitting a PR:
- Ensure all packages build without errors:
pnpm build - Test your changes in the example project:
examples/vite-react-ts - Run any available tests in the affected packages
Feel free to open an issue for any questions or discussions about contributing!
MIT License - see LICENSE for details.
This monorepo contains the following packages:
@flairjs/core- Core transformation engine (Rust + NAPI)@flairjs/client- Client-side utilities and types@flairjs/bundler-shared- Shared bundler utilities@flairjs/vite-plugin- Vite integration@flairjs/rollup-plugin- Rollup integration@flairjs/webpack-loader- Webpack integration@flairjs/parcel-transformer- Parcel integration