TL;DR: Fluentic Style is a new JSX styling library for React, Preact, Solid, and compatible JSX runtimes. It starts with typed style objects and a css prop, then adds selector chains, component slots, scopes, themes, runtime resolution, and optional static CSS extraction for production builds.

Fluentic started as an object-first styling API.

That was intentional.

The original shape was built around typed style objects, explicit composition, selector chains, component slots, scopes, themes, and optional static extraction later.

So the first Fluentic style looked more like this:

import { style } from '@fluentic/style';

const button = style({
  display: 'inline-flex',
  alignItems: 'center',
  gap: 8,
  border: 0,
  borderRadius: 8,
  paddingInline: 16,
  paddingBlock: 8,
  backgroundColor: '#2563eb',
  color: '#ffffff',
}).hover({
  backgroundColor: '#1d4ed8',
});

Enter fullscreen mode Exit fullscreen mode

This is still the Fluentic style I personally reach for first.

It is regular TypeScript. The style is data. The chain adds a state. The result can work at runtime, and the compiler can extract the static parts later.

So when I started thinking about Tailwind support, my first instinct was pretty predictable:

Bring Tailwind’s vocabulary into Fluentic’s object-based style world.

And then, somehow, I ended up adding two Tailwind presets.

The docs for both live here:

The First Preset Was Object-Based

The first version was the Fluentic-shaped answer.

Instead of regular CSS property names, it used a Tailwind-like object dialect:

const button = tw({
  inlineFlex: true,
  items: 'center',
  gap: '$2',
  px: '$4',
  py: '$2',
  bg: '$blue.600',
  text: '$white',
  rounded: '$md',
}).hover({
  bg: '$blue.700',
}).md({
  px: '$6',
});

Enter fullscreen mode Exit fullscreen mode

I still like this API.

It keeps a lot of the things I wanted Fluentic to care about:

  • styles are grouped as objects
  • TypeScript can understand the shape
  • dynamic branches can affect several declarations together
  • selectors and media stay in chain methods
  • the output is still Fluentic style data

It also keeps the useful part of Tailwind’s vocabulary: px, py, bg, rounded, items, gap, theme scales, breakpoints, and so on.

But there is a tradeoff.

A Tailwind user still has to translate from the syntax they already know:

inline-flex        -> inlineFlex: true
items-center       -> items: 'center'
gap-2              -> gap: '$2'
bg-blue-600        -> bg: '$blue.600'
hover:bg-blue-700  -> .hover({ bg: '$blue.700' })

Enter fullscreen mode Exit fullscreen mode

That translation is not awful.

But it is still translation.

And if the point of a Tailwind preset is to make Tailwind-oriented codebases easier to adopt, asking every user to move through an object dialect is not always the best bridge.

That was the interesting part.

The first preset matched Fluentic’s original API taste.

But it did not fully match Tailwind’s muscle memory.

So I Added A Class-Name Preset Too

The second preset keeps class names closer to the way Tailwind users already write them:

const button = tw(
  'inline-flex',
  'items-center',
  'gap-2',
  'rounded-md',
  'bg-blue-600',
  'px-4',
  'py-2',
  'text-white',
)
  .hover('bg-blue-700')
  .md('px-6');

Enter fullscreen mode Exit fullscreen mode

This feels much closer to Tailwind.

But it is still Fluentic.

The variants are not packed into the class string:

tw('bg-blue-600')
  .hover('bg-blue-700')
  .md('px-6');

Enter fullscreen mode Exit fullscreen mode

Instead of:

bg-blue-600 hover:bg-blue-700 md:px-6

Enter fullscreen mode Exit fullscreen mode

That part is deliberate.

Fluentic already has chain methods for states, selectors, media queries, scopes, and other styling conditions. I did not want Tailwind support to bypass that.

So class names became the input vocabulary, but chains still carry the structure.

In practice, it looks like this:

class names    -> transform -> Fluentic style data
chain methods  -> selectors, media, and conditions
runtime        -> resolves dynamic styles
compiler       -> extracts static styles
debug tools    -> trace generated CSS back to source

Enter fullscreen mode Exit fullscreen mode

The syntax changed.

The rest of Fluentic stayed the same.

The class-name chain is not just a class string helper either. It also supports the same slot and scope shape as the object-style chain.

The Part That Changed My Mind

Before this, I mostly thought about Fluentic as an object-style API with chain methods.

After adding both Tailwind presets, that felt too narrow.

The object API is still important. It is still the default path. It is still the one I prefer for complex component styling.

But Fluentic can support more than one authoring style.

The input can be regular CSS objects:

style({
  backgroundColor: '#2563eb',
}).hover({
  backgroundColor: '#1d4ed8',
});

Enter fullscreen mode Exit fullscreen mode

It can be a Tailwind-like object dialect:

tw({
  bg: '$blue.600',
}).hover({
  bg: '$blue.700',
});

Enter fullscreen mode Exit fullscreen mode

It can be a Tailwind-like class-name dialect:

tw('bg-blue-600').hover('bg-blue-700');

Enter fullscreen mode Exit fullscreen mode

And projects can define their own naming and transform logic too.

That last part matters most to me.

Fluentic does not need to own every word in the styling language.

It needs to provide the pieces underneath:

  • style values
  • selector chains
  • slots
  • scopes
  • tokens
  • runtime resolution
  • compiler extraction
  • debugging metadata

The vocabulary on top can be different per team.

If you want to see the lower-level APIs behind that idea, these docs are the useful ones:

Why Not Just Use Tailwind?

Fair question.

Tailwind is good. A lot of people are fast with it. The naming system is familiar, the docs are strong, and the ecosystem is huge.

I did not add Tailwind presets because I think Fluentic should replace Tailwind.

I added them because Tailwind-style vocabulary is useful, and I wanted to see what happens when that vocabulary runs through Fluentic’s component styling system.

For example, Fluentic still has component slots with the class-name chain:

const card = {
  root: tw.slot('rounded-xl', 'bg-white', 'p-6'),
  title: tw.slot('text-xl', 'font-bold'),
  body: tw.slot('text-slate-600'),
};

Enter fullscreen mode Exit fullscreen mode

And scopes can still target those slots:

const compact = tw.scope([
  card.root('p-4'),
  card.title('text-lg'),
  card.body('text-sm'),
]);

Enter fullscreen mode Exit fullscreen mode

That is the part I care about.

The class names are not the component contract.

The slots are.

The component can decide which parts are styleable without exposing its private DOM structure.

Where This Leaves Fluentic

I did not want a Tailwind preset.

Then I added one.

Then I added another one because the first one was a little too loyal to Fluentic’s original object-first API.

That feels like a useful library-design lesson.

Sometimes the API you prefer is not the API that helps people cross the bridge.

And sometimes supporting another way to write styles does not weaken the idea.

It clarifies what the library is actually responsible for.

Fluentic now supports:

  • object-based style chains
  • class-name-based style chains
  • custom selectors
  • custom transforms
  • Tailwind-like presets on top

Both Tailwind presets still go through the same Fluentic path: chains, slots, scopes, runtime resolution, extraction, and debugging.

So no, Fluentic did not become Tailwind.

But adding Tailwind support made Fluentic more honest about itself.

Fluentic is not trying to own every styling word you write.

It is trying to make those words composable once they enter your component system.


Fluentic Style is still new and currently in beta. The core authoring model is usable today, but I am still looking for early users to try it in real React, Preact, Solid, and component-library codebases.

If this direction is interesting, here are the main links:

Feedback would help a lot right now, especially around API shape, Tailwind preset coverage, build integrations, debugging, and whether the slot/scope model makes sense in real component work.