I started Fluentic Style with a small idea:
I wanted React Native-style composition in React web.
<View style={[base, active && activeStyle, props.style]} />
That kind of style composition always felt practical to me.
You have a base style.
You add another style when some state is active.
You still let styles come from props.
So the first Fluentic idea looked like this:
<div css={[base, active && selected, props.css]} />
That was the starting point.
I wrote about it here:
Bringing React Native Style Composition to React
But the more Fluentic evolved, the clearer it became that the real problem was never just CSS-in-JS or build-time extraction.
What felt missing was a better pattern for component-level styling, and for theming that can move from app-wide defaults down to a specific component.
The bigger question became:
Can a styling system cover the everyday path from styling one element, to styling reusable components, to component themes, to app-wide themes, while still keeping debugging and production extraction usable?
That is the version of Fluentic I am building now.
Not just another CSS-in-JS syntax.
More like an attempt to connect the parts of styling that often feel split apart.
The Problem Is Not Only Putting CSS In JS
CSS-in-JS gave frontend developers something useful.
It made styles feel closer to components.
It made dynamic styling easier.
It made local styling easier.
It made React UI feel less split between a component file and a separate stylesheet.
But daily app styling is bigger than:
How do I put CSS on this DOM element?
That is one layer.
A real app usually moves through several layers:
element style
component style
component variant
component theme
app-wide theme
production output
debugging
Many styling approaches are good at one or two of those.
Some are great for quick element styles.
Some are great for app-wide tokens.
Some are great for utility classes.
Some are great for local CSS files.
Some are great for runtime dynamic values.
Some are great for extracted CSS.
But in real app work, I kept wanting those layers to connect better.
That became the bigger Fluentic goal:
style composition from elements, to components, to component themes, to app-wide themes
without turning each layer into a separate styling system.
Start With One Element
The smallest case should stay small.
If I only need to style one element, I should be able to write one style and attach it.
import { style } from '@fluentic/style';
const saveButton = style({
borderRadius: 8,
padding: '8px 12px',
});
<button css={saveButton}>Save changes</button>;
If that element needs hover, media, or state styles, it should still stay attached to the same style idea:
const saveButton = style({
borderRadius: 8,
padding: '8px 12px',
}).hover({
backgroundColor: '#f3f4f6',
});
<button css={saveButton}>Save changes</button>;
For teams that like utility-class authoring, Fluentic can also work closer to that style:
const saveButton = cx(
'inline-flex',
'items-center',
'rounded-md',
'px-3',
'py-2',
).hover('bg-slate-100');
<button css={saveButton}>Save changes</button>;
So Fluentic does not start by forcing every use case into components, themes, or design-system structure.
Sometimes you just need to style an element.
That should be easy.
Then The Element Becomes A Component
The next problem appears when that styled element becomes reusable.
A button is not always just one DOM node.
It can have parts:
function Button(props) {
return (
<button>
<span>{props.icon}</span>
<span>{props.children}</span>
</button>
);
}
Now the styling question changes.
There is a root.
There is an icon.
There is a label.
Maybe a loading indicator.
Maybe an action area.
Maybe a wrapper around some third-party element.
Frontend teams already think in these names:
root, icon, label, content, trigger, item, control, thumb, track, portal
The old HTML/CSS way would usually name those parts with classes:
.button { ... }
.button__icon { ... }
.button__label { ... }
.button[data-danger] .button__label { ... }
That is understandable.
But in a component system, the component owns the inside.
So when the outside needs to style those parts, React APIs often grow shapes like this:
<Button
className="danger-button"
iconClassName="danger-icon"
labelClassName="danger-label"
>
or:
<Button
classes={{
root: 'danger-button',
icon: 'danger-icon',
label: 'danger-label',
}}
>
or:
<Button
styles={{
label: { fontWeight: 700 },
}}
>
These are practical. I have written APIs like this many times.
But the repeated shape is clear:
the component has parts, and outside code needs a supported way to style those parts
Fluentic’s answer is style.slot(...).
A slot is a normal style for the component itself, but it also gives that component part an identity that outside styles can target later.
const buttonStyles = {
root: style.slot({
borderRadius: 8,
padding: '8px 12px',
}),
icon: style({
flex: '0 0 auto',
}),
label: style.slot({
fontWeight: 700,
}),
};
Notice that not every internal element needs to be a slot.
icon can stay private with style(...).
root and label become public styling targets with style.slot(...).
That is the first important split:
use style(...) for ordinary element styles
use style.slot(...) when a component part should be styleable from outside
Component Themes Are Not The Same As App Themes
Once a component exposes slots, outside code needs a way to provide styles for those slots.
That is where style.scope(...) comes in.
A scope groups changes for public component parts:
const dangerButton = style.scope([
buttonStyles.root({
backgroundColor: '#dc2626',
}),
buttonStyles.label({
color: 'white',
}),
]);
The component receives that as a theme-like input:
type ButtonProps = {
children: React.ReactNode;
icon?: React.ReactNode;
theme?: StyleTheme;
};
Then the component prepares the styles it will render:
function Button(props: ButtonProps) {
const css = combineStyle(
buttonStyles,
bindScope(buttonStyles.root, props.theme),
);
return (
<button css={css.root}>
{props.icon ? <span css={css.icon}>{props.icon}</span> : null}
<span css={css.label}>{props.children}</span>
</button>
);
}
This is different from passing a raw class name into each part.
The outside says what it wants to change:
<Button theme={dangerButton}>Delete</Button>
The component still decides where those styles attach.
That is why I think of scopes as component-facing themes.
They are not only global colors.
They can change slots, tokens, states, media rules, and multiple component parts together.
App-Wide Themes Need A Different Layer
App-wide theming is related, but it is not exactly the same problem.
Sometimes the app wants to define shared values:
const color = createTokens({
surface: '#ffffff',
text: '#111827',
accent: '#2563eb',
danger: '#dc2626',
});
A style can use those tokens:
const page = style({
backgroundColor: color.surface,
color: color.text,
});
Then an app theme can override those token values:
const darkTheme = createTheme([
color.surface('#0f172a'),
color.text('#f8fafc'),
]);
That is app-facing theming.
It answers:
What should surface, text, or accent mean in this part of the app?
A component theme answers a different question:
How should this component’s public parts or component-specific tokens change?
Fluentic supports both.
Use createTheme(...) when the theme only changes token values.
Use style.scope(...) when the theme needs to affect component slots, component tokens, states, media conditions, or multiple parts together.
This distinction matters because app-wide themes and component themes often live in different realms.
A design system may define app tokens.
A component library may expose component tokens and slots.
An app may need to map app values into component values.
For example:
const button = createTokens({
color: {
surface: '#2563eb',
surfaceHover: '#1d4ed8',
text: '#ffffff',
},
});
const appButtonTheme = style.scope([
button.color.surface(color.accent),
button.color.surfaceHover(color.accent),
button.color.text('#ffffff'),
]);
The app theme controls what color.accent means.
The component theme says how the button should use that app value.
That is the kind of bridge I wanted Fluentic to support.
Not app theming over here, component theming over there, and manual glue in between.
One styling path that can connect them.
Variants Become Scopes Too
Component variants are usually where styling logic starts getting busy.
A variant like this looks simple:
<Button size="sm" tone="danger" />
But internally it may affect several parts:
- root padding
- label font size
- icon size
- background color
- hover color
- disabled color
Without a shared styling shape, that often becomes a matrix of class names or conditionals.
In Fluentic, a variant can be a scope:
const compactButton = style.scope([
buttonStyles.root({
gap: 6,
paddingBlock: 6,
paddingInline: 10,
}),
buttonStyles.label({
fontSize: 13,
}),
]);
const dangerButton = style.scope([
buttonStyles.root({
backgroundColor: '#dc2626',
}),
buttonStyles.label({
color: 'white',
}),
]);
And because scopes are values, they compose:
<Button
theme={[
compact && compactButton,
danger && dangerButton,
props.theme,
]}
>
Delete
</Button>
This is where Fluentic starts feeling bigger than element-level CSS-in-JS.
The same composition idea can move from:
<div css={[base, active && selected]} />
to:
<Button theme={[compact && compactButton, danger && dangerButton]} />
The shape stays familiar.
The layer changes.
Debugging Still Has To Follow The Whole Path
A nice authoring API is not enough if generated CSS becomes a dead end.
That is one of the places where I think styling tools often ask developers to accept too much friction.
You write nice code.
DevTools shows generated class names or injected CSS.
Then you have to guess where the rule came from.
Fluentic emits atomic CSS, but not only for output size or dedupe.
Atomic CSS also gives a useful debugging unit:
one generated rule can map back to one styling decision
For example:
style({
backgroundColor: '#2563eb',
}).hover({
backgroundColor: '#1d4ed8',
});
A generated hover background-color rule should be able to point back to the backgroundColor inside .hover(...).
For slots and scopes, the trace should stay meaningful too.
If this wins:
const dangerButton = style.scope([
buttonStyles.label({
color: 'white',
}),
]);
the generated color rule should be able to lead back to the scoped label override.
That is why Fluentic keeps debug metadata and sourcemap information in development.
The debug story should follow the same path:
style → slot → scope → generated CSS → source trace
If component styling is part of the model, debugging component styling has to be part of the model too.
The other big requirement is production.
A runtime-only CSS-in-JS library can be convenient, especially in small apps.
But production apps need more.
They need extracted CSS.
They need SSR and framework integrations.
They need predictable output.
They need the runtime to do less work when the build can prepare things ahead of time.
Fluentic’s compiler and bundler plugins extract the static style declarations from patterns like:
style(...)
style.slot(...)
style.scope(...)
and emit extracted atomic CSS plus prepared JavaScript output.
But real apps still have runtime-known choices:
<Button
theme={[
compact && compactButton,
danger && dangerButton,
props.theme,
]}
>
Delete
</Button>
The build can prepare CSS from the style declarations.
The runtime still resolves which themes, scopes, token values, and prop-driven choices apply during render.
That is important.
I do not want a system where extracted CSS only works for the boring cases, and the moment a component becomes dynamic you move to a different styling model.
The goal is one authoring model:
build extracts what can be prepared
runtime resolves what the app decides while rendering
Syntax Should Be Flexible, But The Path Should Stay Connected
CSS-in-JS discussions often become syntax discussions.
Objects.
Tagged templates.
Utility classes.
Inline styles.
Generated classes.
CSS files.
I understand why. Syntax is what developers touch every day.
Fluentic started object-first:
const card = style({
padding: 16,
borderRadius: 12,
});
But Fluentic now also supports Tailwind-like class-name authoring:
const card = cx('rounded-xl', 'bg-white', 'p-4');
It also supports Tailwind-style object presets and custom transforms.
A design system can create its own vocabulary:
const panel = ui({
row: true,
center: true,
gapX: 12,
});
The point is not that every team should write styles the same way.
The point is that different syntaxes can still feed the same Fluentic path.
Once they become Fluentic styles, they can still participate in:
- element styling
- slots
- scopes
- token themes
- component themes
- atomic CSS output
- production extraction
- sourcemaps
- debug metadata
That is the practical reason custom transforms matter.
They are not only a syntax trick.
They let a team adapt the authoring vocabulary without leaving the rest of the styling system behind.
Frameworks Need Bridges, Not A New Styling Model
Fluentic started with React.
React is still the main focus.
But modern frontend is not only one JSX/runtime story.
Next.js App Router brings server components, server/client rendering, HMR, streaming concerns, development CSS, sourcemaps, and production extraction.
SolidJS has its own compiler-centered JSX path.
Preact has a different prop shape.
So Fluentic is moving toward a stable styling core with framework-specific bridges.
React can use Fluentic’s JSX runtime, or it can use a cssProp compiler transform.
Next.js gets a deeper integration for App Router, RSC development styling, debug CSS, sourcemaps, and production extraction.
SolidJS client-side support uses a css prop transform before Solid’s compiler.
The goal is:
keep the style path stable, adapt the framework handoff
That keeps Fluentic from being only a React runtime library.
The style ideas can stay the same while the JSX integration changes per framework.
What Fluentic Currently Has
Fluentic Style is still new and currently in beta.
But it already covers much more than my first small idea.
Authoring:
style(...) object styles
cx(...) class-name authoring
- Tailwind-like class-name preset
- Tailwind-style object preset
- custom style functions
- custom transforms
Element and component composition:
css prop arrays
- conditional styles
style.slot(...) for public component parts
style.scope(...) for component themes and slot overrides
combineStyle(...)
bindScope(...)
Theming:
createToken(...)
createTokens(...)
createTheme(...) for token-only themes
style.scope(...) for component themes, slot overrides, and token mapping
Build and debug:
- atomic CSS in development and production
- production CSS extraction
- prepared JavaScript output
- debug metadata
- sourcemap support
- development utilities for inspecting generated styles
Frameworks:
- React
- Next.js App Router
- SolidJS client-side support
- Preact-oriented adapter path
That is far beyond the original idea.
The first goal was:
bring React Native-style composition to React web
The current goal is closer to:
make everyday styling composition work from element styles to component styles to component themes to app-wide themes, with debugging and production output still part of the same story
That is the part that keeps me building it.
Closing
I do not think existing CSS-in-JS approaches are wrong.
I do not think CSS is broken.
A lot of styling approaches are useful, and I still use many familiar patterns.
Fluentic is my attempt to connect the parts that I kept seeing split apart:
- quick element styling
- component styling
- component theming
- app-wide theming
- style composition
- generated CSS debugging
- production extraction
- framework integration
- flexible authoring syntax
It is still beta, and I expect the API to keep improving.
But the direction is clear now.
Fluentic is no longer only my attempt to bring React Native-style arrays to React web.
It is an attempt to make styling feel more complete for component-based apps: from one element, to a reusable component, to local component themes, to app-wide themes, while still caring about CSS output, DevTools, production builds, and framework realities.
Useful links: