@fluixi-ui/fx

中文文档
0.1.0-alpha.12发布于 2026/11/05MITTypeScriptES Module近 30 天下载 4477

Token-aware style objects for the Fluixi UI ecosystem — the fx prop engine

原始使用文档
文档目录12 节
  1. Values
  2. Selectors
  3. Responsive
  4. Container queries
  5. Colour
  6. Ready-made objects
  7. Animation
  8. How it lands on the page
  9. Server rendering
  10. Composing
  11. Binding it to an element
  12. License

@fluixi-ui/fx

Token-aware style objects for Fluixi UI — the engine behind the fx prop.

<Box fx={{ p: 4, bg: 'bgSubtle', radius: 'lg', _hover: { bg: 'bgMuted' } }} />

No CSS-in-JS runtime, no dependencies. Values resolve to the --flx-ui-* custom properties @fluixi-ui/tokens defines, so anything written with fx follows the theme, the accent, and the skin — including a theme driven from @fluixi-css.

Values

A property's scale decides both which tokens a string key resolves against and what a bare number means:

p: 3          // padding: var(--flx-ui-space-3)   — space scale
m: -2         // margin: calc(var(--flx-ui-space-2) * -1)
width: 240    // width: 240px                     — sizing is pixels
zIndex: 3     // z-index: 3                       — unitless stays bare
radius: 'lg'  // border-radius: var(--flx-ui-radius-lg)
bg: 'primary' // background-color: var(--flx-ui-color-primary)
bg: 'accent-9'// background-color: var(--flx-ui-accent-9)
bg: '#ff0000' // background-color: #ff0000        — raw CSS always allowed

Aliases are deliberately few — p px py m mx my bg radius shadow. Everything else is the real CSS property name (paddingTop, not pt), so there's no table to learn. px/py map to the logical properties and follow writing direction.

Selectors

A closed set of _-prefixed keys. They expand to the library's real conventions, not just the CSS pseudo — headless parts signal state through data-*/ARIA:

_disabled: { opacity: 'disabled' }
// :disabled, [disabled], [data-disabled]

State: _hover _active _focus _focusVisible _focusWithin _disabled _checked _selected _expanded _open _invalid Pseudo-elements: _before _after _placeholder _selection Structural: _first _last _odd _even _notFirst _notLast _children Environment: _dark _light _hc _rtl _motionReduce _motionSafe

_dark targets [data-theme='dark'] — the axis createTheme drives — and matches the element itself as well as a themed ancestor, so scoped theming works.

The set being closed is the point: fx can't reach an app's class names, so a component can never couple itself to consumer CSS.

Nesting composes:

_dark: { _hover: { bg: 'primaryHover' } }

Responsive

Mobile-first objects keyed by breakpoint. base carries no media query:

fx={{ p: { base: 3, md: 5 } }}

Container queries

Respond to the space a component is actually given rather than the viewport. The container is an ancestor — an element can't query itself:

<Card fx={{ containerType: 'inline-size' }}>
  <Stack fx={{ _container: { md: { flexDirection: 'row' } } }} />
</Card>

Keys are a breakpoint name or a raw condition. Element-relative sizes often don't line up with the page scale, so raw conditions are first-class:

_container: { '(min-width: 30rem)': { gap: 4 } }

Name a container when the nearest one isn't the one you mean:

<aside fx={{ containerType: 'inline-size', containerName: 'panel' }}>
  <div fx={{ _container: { name: 'panel', lg: { p: 6 } } }} />
</aside>

At-rules nest rather than combine, so a container query and a media query can wrap the same declarations.

Colour

Helpers emit CSS; they never compute a colour in JS. That matters here: tokens are custom properties that re-resolve against data-theme / data-accent, so a value computed at render would freeze whichever theme happened to be active. color-mix() and relative colour syntax re-resolve on their own, so a derived colour keeps following the theme, the accent and the skin.

import { alpha, mix, lighten, darken } from '@fluixi-ui/fx';

alpha('primary', 12)      // color-mix(in oklab, var(--flx-ui-color-primary) 12%, transparent)
mix('primary', 'bg', 20)  // 20% primary over the page background
lighten('primary', 8)     // oklch(from … calc(l + 0.08) c h / alpha)
darken('border', 10)

Every amount is a percentage, 0–100. lighten/darken shift lightness in OKLCH, which keeps chroma and hue — a tint stays the same colour instead of drifting grey the way mixing with white or black does. They need relative colour syntax (Chrome 119+, Safari 16.4+, Firefox 128+); for a wider floor use mix(color, 'white', n).

Ready-made objects

An fx value is a plain object, so a utility is just a named one — import it, spread it, merge it:

import { flexBetween, muted, bordered, clickable, truncate, mergeFx } from '@fluixi-ui/fx';

<div {...fxProps(() => mergeFx(bordered, clickable, { p: 3 }))} />
  • utilities — the everyday one-liners: flex*, fullWidth, stickyTop, muted, bold, nowrap, bordered, rounded, pill, scrim, pointer, clickable, focusRing, disabled. The ones that carry a decision are the point: flexFill includes the min-width: 0 a flex child needs before its text will truncate, muted is the token rather than a picked grey, focusRing is :focus-visible only.
  • presets — the recipes that only work as a set: truncate, lineClamp, visuallyHidden, stack, row, center, scrollY, scrollbarHidden, gradientText, aspect.
  • variants — map a component's own props to fx without a ternary chain: later groups win, false turns a defaulted group off.

Animation

keyframes() defines an animation and returns its name; animate() writes the longhands. A bare second argument is the duration.

const pulse = keyframes({ from: { opacity: 0.4 }, to: { opacity: 1 } });

fx={animate(pulse, { repeat: 'infinite' })}
fx={fadeIn('slow')}
fx={slideIn('bottom', { distance: 16 })}

Stop values go through the resolver, so tokens work inside them, and the name is hashed from the resolved body so the same animation defined twice is one rule. The stock set — fadeIn/fadeOut, scaleIn/scaleOut, slideIn/slideOut, spin, pulse, shimmer — registers its rules on first use rather than on import.

animate returns an object rather than the animation shorthand string, so it merges with what surrounds it instead of resetting the longhands it doesn't mention.

How it lands on the page

Flat declarations and every base value go to the style attribute — most fx usage never touches the stylesheet. Only selector keys and non-base breakpoints need a rule; those are hashed, deduped, and inserted under [data-fx="…"] into @layer flx.fx, which sits above components and skins so fx wins, and below unlayered consumer CSS so an app can still override it.

Server rendering

Inline declarations need nothing. For the rule tier, emit what was collected:

const { css, tokens } = collectFx();
`<style data-fx-sheet data-fx-tokens="${tokens}">${css}</style>`;

The client adopts that sheet instead of re-inserting its rules. resetFx() clears a request's rules but keeps the animations — those are registered once at module scope, so dropping them would leave every request after the first naming an animation nobody emits.

Composing

mergeFx folds a component's own styling, its sp entries, and the consumer's fx into one object — later wins per property, selector blocks and breakpoint records merge rather than replace:

resolveFx(mergeFx(componentOwn, sp.body?.fx, props.fx));

A compound component reaches its parts with slot, which takes both channels: the part's own sp entry, and the styles a caller addressed to it through fx.sp.

<PageBody {...slot('body', local.fx, local.sp)} />

Binding it to an element

const [local, others] = splitProps(props, ['fx', 'style']);
return <div {...fxProps(() => local.fx, () => local.style)} {...others} />;

Pass accessors, not values. The resolve runs inside a memo, so a reactive fx updates without re-rendering. A style prop is merged last and stays raw CSS — no token resolution — so it remains the lowest-level escape hatch.

License

MIT

版本与发布记录

@fluixi-ui/fx 共发布 13 个版本,首次发布于 2026/08/05;过去 12 个月发布了 13 个版本。

版本发布日期距今
0.1.0-alpha.12latest2026/11/0510 天前
0.1.0-alpha.112026/11/0510 天前
0.1.0-alpha.102026/11/0114 天前
0.1.0-alpha.92026/10/3016 天前
0.1.0-alpha.82026/10/131 个月前
0.1.0-alpha.72026/10/051 个月前

关于 @fluixi-ui/fx 的常见问题

@fluixi-ui/fx 是什么?

Token-aware style objects for the Fluixi UI ecosystem — the fx prop engine

如何安装 @fluixi-ui/fx?

在项目目录运行 npm install @fluixi-ui/fx,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@fluixi/reactive(>=1.0.0-alpha.41)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@fluixi-ui/fx 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fluixi-ui/fx 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fluixi-ui/fx 可以商用吗?

@fluixi-ui/fx 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@fluixi-ui/fx 的下载量有多少?

根据 npm 官方下载统计,@fluixi-ui/fx 在 2026/10/15 – 2026/11/13 这 30 天内被下载 4,477 次,日均约 149 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fluixi-ui/fx 还在维护吗?

最新版本 0.1.0-alpha.12 发布于 2026/11/05(10 天前);至今共发布 13 个版本,其中过去 12 个月发布了 13 个;首次发布于 2026/08/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。