This is part of my Building Fluentic Style series, where I’m writing down the design decisions, tradeoffs, and small surprises from building Fluentic Style.
Fluentic started as a styling model I was exploring for React web apps.
The shape was inspired by the style composition I liked from React Native:
<View style={[base, active && activeStyle, props.style]} />
For web JSX, that became:
<div css={[base, active && activeStyle, props.css]} />
But as Fluentic grew, the architecture became less tied to React itself.
Most of Fluentic lives in plain TypeScript:
style(...)
style chains
style.slot(...)
style.scope(...)
combineStyle(...)
tokens
themes
custom transforms
debug metadata
production extraction
Those pieces do not really care whether the final app is React, Solid, Preact, or another JSX framework.
The framework-specific part is much smaller.
It happens at the point where a style value is attached to a real DOM element.
In Fluentic, that boundary is the css prop:
Fluentic style data
-> css prop
-> framework-specific class/style output
That made me ask a different question:
Can Fluentic keep the same styling model,
but adapt the final css prop handoff for other JSX frameworks?
SolidJS became the first real test of that idea.
Docs:
The Feature
The user-facing goal is simple.
I wanted this to work in SolidJS:
import { style } from '@fluentic/style';
const card = style({
padding: 16,
borderRadius: 8,
backgroundColor: '#ffffff',
}).hover({
boxShadow: '0 12px 30px rgb(15 23 42 / 0.16)',
});
export function App() {
return <div css={card}>Styled by Fluentic.</div>;
}
That gives Solid apps the same Fluentic authoring model:
typed style objects
chain methods for states and media
css prop composition
component slots
scopes
tokens and themes
custom style dialects
debug tooling
production CSS extraction
So this is not only about adding another way to set class.
The interesting part is bringing Fluentic’s style data model to Solid DOM elements.
Why Solid Needed A Different Path
In React, Fluentic can use a custom JSX runtime.
A React app can configure:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@fluentic/style/jsx"
}
}
For plugin builds, React can use:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@fluentic/style/plugin/jsx"
}
}
That fits React’s automatic JSX runtime nicely.
So the first tempting thought was:
React uses a Fluentic JSX runtime.
Maybe Solid needs a Fluentic JSX runtime too?
But SolidJS is not React with a different import.
Solid has its own compiler path. In a Vite app, Solid usually goes through vite-plugin-solid, and the official TypeScript setup keeps JSX preserved for Solid’s compiler:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"]
}
}
That jsxImportSource points to solid-js.
I did not want Fluentic support to mean replacing that path with a Fluentic one and hoping everything still behaves like Solid.
The safer shape was:
Let Solid compile Solid JSX.
Let Fluentic handle only css={...}.
The Small Transform
The Solid integration uses the Fluentic Vite plugin before vite-plugin-solid:
import { CssPropPresets, plugin as stylePlugin } from '@fluentic/style/plugin/vite';
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid';
export default defineConfig({
plugins: [
stylePlugin({
cssProp: CssPropPresets.Solid,
}),
solid(),
],
});
The order matters.
your TSX
-> Fluentic sees css={...} while it is still JSX
-> Fluentic rewrites css on DOM JSX elements
-> vite-plugin-solid compiles the remaining Solid JSX
Solid still handles Solid JSX.
Fluentic only handles the styling handoff.
That is the whole integration idea.
What Gets Rewritten
Solid DOM elements use class, not React’s className.
So CssPropPresets.Solid tells Fluentic:
class prop -> "class"
style prop -> "style"
style mode -> "solid"
adapter -> "@fluentic/style/adapter/solid"
If you write:
<div
{...props}
class="base"
style={styleObj}
css={styles.root}
data-id="x"
/>
Fluentic rewrites that DOM JSX element before Solid compiles it.
Conceptually, it becomes something like this:
import { mergeJsxProps as _fluenticMergeJsxProps } from '@fluentic/style/adapter/solid';
<div
{..._fluenticMergeJsxProps([
props,
{
class: 'base',
style: styleObj,
css: styles.root,
'data-id': 'x',
},
])}
/>
The exact generated code can change, but the shape is the point.
Fluentic gathers the props that need merging, then passes them to the Solid adapter.
The adapter resolves the Fluentic style value and returns normal Solid-compatible props:
existing spread props
existing class
existing style
Fluentic css prop
other DOM props
-> Solid adapter
-> class/style props Solid understands
So this authoring code:
<div class="base" style={styleObj} css={styles.root} />
behaves like:
base class
+ Fluentic generated class
+ existing style object
+ runtime style output if Fluentic needs it
Solid does not need to understand Fluentic style data.
By the time Solid compiles the JSX, the DOM element has already been adapted back into the prop shape Solid expects.
A Real Before And After
Here is a small Solid component:
import { style } from '@fluentic/style';
const styles = {
root: style({
color: 'red',
}).hover({
color: 'blue',
}),
};
export function View(props: { active?: boolean }) {
const localStyle = {
opacity: props.active ? 1 : 0.6,
};
return (
<div
{...props}
class="base"
style={localStyle}
css={styles.root}
data-view="root"
/>
);
}
Before vite-plugin-solid compiles the JSX, Fluentic rewrites the DOM element with css={...}.
Conceptually:
import { mergeJsxProps as _fluenticMergeJsxProps } from '@fluentic/style/adapter/solid';
import { style } from '@fluentic/style';
const styles = {
root: style({
color: 'red',
}).hover({
color: 'blue',
}),
};
export function View(props: { active?: boolean }) {
const localStyle = {
opacity: props.active ? 1 : 0.6,
};
return (
<div
{..._fluenticMergeJsxProps([
props,
{
class: 'base',
style: localStyle,
css: styles.root,
'data-view': 'root',
},
])}
/>
);
}
Then Solid compiles the JSX normally.
That is the key.
Fluentic does not need to own Solid’s JSX runtime.
It only needs to remove css={...} from the DOM handoff and turn it into class and style.
Why Not Just Set class?
Because css={...} is not just another spelling for class.
A class helper can do this:
<div class={someClassName} />
That is useful, but Fluentic’s css prop carries structured style data.
It can carry conditional style arrays:
<div css={[base, active && selected]} />
It can carry style chains:
const item = style({
color: '#334155',
}).hover({
color: '#0f172a',
});
It can carry component parts:
const listStyles = {
root: style.slot({
display: 'grid',
gap: 8,
}),
item: style.slot({
padding: 8,
}),
};
And it can carry outside styles for those parts:
const compact = style.scope([
listStyles.root({
gap: 4,
}),
listStyles.item({
padding: 6,
}),
]);
It can also participate in production extraction and development debugging.
So the css prop is the place where Fluentic’s style model attaches to a DOM element.
That is why the transform matters.
Why Only DOM JSX Elements?
Fluentic rewrites css={...} on DOM JSX elements:
<div css={styles.root} />
<section css={styles.card} />
<button css={styles.button} />
But it leaves custom component calls alone:
<Button css={styles.root} />
That is deliberate.
A custom component is someone else’s API.
Maybe it accepts css.
Maybe it forwards it.
Maybe it maps it to slots.
Maybe it ignores it.
Fluentic should not guess.
So the boundary stays clear:
DOM JSX element
-> compile css prop to class/style
custom component
-> leave css prop alone as component API
This keeps the Solid integration small and predictable.
Fluentic adapts the DOM handoff.
It does not try to understand every Solid component.
Existing class, style, And Spreads Still Matter
A css prop transform cannot just replace class and style.
Solid code already uses them:
<div
{...props}
class={props.active ? 'is-active' : 'is-idle'}
style={{
opacity: props.active ? 1 : 0.5,
}}
css={styles.root}
/>
A spread can already contain class, style, event handlers, data attributes, or anything else.
So the transform cannot be as naive as:
<div class={getClassName(styles.root).className} />
The actual job is:
preserve normal JSX props
resolve css into class/style
merge existing class with Fluentic class
merge existing style with Fluentic runtime style
leave unrelated props alone
That is why the adapter exists.
The css prop looks small when you write it, but the framework handoff needs to be careful.
Why This Feels Right For Solid
The TypeScript setup stays Solid-shaped:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"]
}
}
The plugin order tells the story:
Fluentic handles css={...}
Solid handles Solid JSX
And the styling model stays Fluentic:
style(...)
style chains
style.slot(...)
style.scope(...)
combineStyle(...)
tokens
themes
custom transforms
class-name chains
debug utilities
production extraction
Only the final handoff changed.
For React, that handoff can happen through a Fluentic JSX runtime.
For Solid, it happens through a css prop transform before vite-plugin-solid.
For Preact, another compiler config preset can adapt to its prop shape.
The common idea is:
Fluentic owns the style data.
The framework owns rendering.
The adapter translates at the DOM prop boundary.
That is the part that made this integration feel bigger than Solid alone.
If another JSX-based framework has a clear DOM prop shape, Fluentic may not need to become that framework.
It may only need to adapt the css prop at the right boundary.
A More Flexible JSX Integration
The SolidJS work also made Fluentic’s JSX integration more flexible in general.
Before this, React mostly used Fluentic through the JSX runtime setup.
That still works, and it is still a good option when you want Fluentic to handle the css prop through jsxImportSource:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@fluentic/style/plugin/jsx"
}
}
But the Solid integration proved another setup can work too:
keep the framework's normal JSX setup
let Fluentic transform css={...} on DOM JSX elements
send the props through a framework adapter
let the framework compiler/runtime do the rest
That option is now available for React too.
So if a React app does not want to use Fluentic’s custom JSX runtime, or it already needs another JSX runtime setup, it can use the cssProp transform instead:
import { CssPropPresets, plugin as stylePlugin } from '@fluentic/style/plugin/vite';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
stylePlugin({
cssProp: CssPropPresets.React,
}),
react(),
],
});
In that setup, React keeps its normal JSX runtime.
Fluentic only compiles the css prop on DOM JSX elements and routes the result through the React adapter.
So React now has two integration options:
Fluentic JSX runtime
-> useful when you want css prop support through jsxImportSource
cssProp compiler transform
-> useful when you want to keep the framework's normal JSX setup
The same adapter idea is what makes Solid support possible.
It also gives Fluentic a cleaner route for other JSX frameworks.
The built-in compiler config presets currently cover:
CssPropPresets.React
CssPropPresets.Preact
CssPropPresets.Solid
And if another JSX framework needs a different DOM prop shape, Fluentic can use a custom adapter.
A custom adapter exports mergeJsxProps, and the compiler routes transformed JSX props through it.
Docs:
This is the part that makes the Solid work feel important beyond Solid.
It pushed Fluentic toward a styling core that can stay stable while the JSX bridge adapts per framework.
Development And Production
Solid apps can enable Fluentic dev utilities before rendering:
import { enableStyleDevUtils } from '@fluentic/style/dev';
import { render } from 'solid-js/web';
import { App } from './App';
if (import.meta.env.DEV) {
enableStyleDevUtils();
}
render(() => <App />, document.getElementById('root')!);
This adds the browser-side helpers Fluentic uses for development inspection: checking generated style usage, switching sourcemap tracing mode, and toggling element markers.
The practical benefit is that generated CSS stays easier to debug. You can inspect what Fluentic generated, trace rules back toward the authored style code, and turn DOM element markers on when you need to see which JSX element produced a generated class.
More details:
The same compiler path can also participate in production extraction.
That matters because I did not want Solid support to mean “runtime-only mode forever.”
The app still writes Fluentic style chains.
The production build can still emit extracted CSS and prepared JavaScript output.
Runtime-known choices can still resolve through Fluentic’s runtime.
So Solid support is not only:
make css prop work visually
It is also:
make css prop fit the same Fluentic extraction/debug/runtime path
Current Scope
One important note: the current SolidJS support is for client-side Solid apps.
That means a Vite + Solid app can use Fluentic’s css prop transform, style chains, slots, scopes, themes, development utilities, and production extraction.
SolidStart and Solid SSR are not something I have properly tested yet.
They will likely need more dedicated integration work. SSR support is a different problem from client-side Solid because it needs a bridge for server-rendered styles, hydration, streamed output, and development debugging.
So for now, I want to describe the current support clearly: Vite + client-side Solid first.
For React, Fluentic already has a deeper Next.js App Router integration covering server/client development, RSC, HMR, sourcemaps, and production extraction. I wrote about that here:
Building Fluentic Style: Making CSS Debugging Work Across Next.js Server and Client
The goal is to bring the same level of seriousness to Solid SSR later, but the current Solid support should be read as client-side Solid support first.
The Result
SolidJS support is small on purpose.
That is what I like about it.
It does not replace Solid’s JSX setup.
It does not ask Solid users to pretend they are writing React.
It does not fork Fluentic’s style model.
It just transforms the one thing Fluentic owns:
css={...}
and lets Solid keep doing the rest.
That boundary is enough to bring Fluentic style chains, slots, scopes, themes, debugging, and extraction into Solid apps.
Fluentic Style is still new and currently in beta. I am looking for early users to try it in real SolidJS, React, Next.js, Preact, and component-library codebases.
Useful links:
- Docs
- SolidJS Integration
- Runtime Interop
- Integration Options
- Plugin Options And Custom Adapters
- SolidJS TypeScript Setup
- GitHub
- npm: @fluentic/style
Feedback would help a lot right now, especially from SolidJS users. I would love to know whether this css prop transform feels natural in real Solid apps, and what styling patterns people want beyond Tailwind-style utility classes.
Top comments (0)