@union-web/react-plugin#
The React plugin provides runtime style resolution for Union themes. It adapts Union's C++ core concepts (elements, selectors, queries) into React hooks and context, enabling components to resolve their styles on the fly based on role, state, and position in the element tree.
Note: This project uses a ratcheting development style. The SolidJS plugin is the currently active runtime. The react-plugin may lag behind in architecture:
- Ancestor state propagation — Solid's
AncestorElementDataincludesstates; the react-plugin omits it, breaking selectors liketoolbar:disabled > button.- Typed style props — Solid uses
UnionStyleProps+StyledRect(preserves Union's semantic model); the react-plugin uses flattenedstyleGroupToCss→ raw CSS objects.unionIR/unionLint— These Vite plugins have been extracted to@union-web/vite-pluginand re-exported from here. They are shared, not duplicated.
How It Adapts Union's API#
The web has no built-in element tree. The plugin recovers hierarchy through explicit ancestor context:
| Union Concept | React Plugin Equivalent |
|---|---|
Union::Element |
createElement(role, opts) inside useUnion() |
Union::ElementQuery |
resolveElement() from core |
| Widget tree (implicit in Qt) | UnionProvider chain via React context |
Union::StyleRule collection |
StyleRule[] from UnionIRContext |
Components call useUnion() and wrap children in the returned UnionProvider to extend the ancestor chain. This enables selectors like textarea > .placeholder to match because Placeholder can see textarea in its ancestor context. Any third-party component can opt in to Union theming by calling useUnion() with the appropriate role.
API Reference#
UseUnionOptions#
interface UseUnionOptions {
role: string;
context?: string[] | null;
state?: string[];
hints?: string[];
attributes?: Record<string, string>;
ids?: string[];
debug?: boolean;
}
| Option | Description |
|---|---|
role |
Element type for matching (e.g., 'button', 'textarea'). Required. |
context |
Explicit ancestor roles. If null, becomes a root element (no ancestors). If unset, inherits from nearest UnionProvider. |
state |
Active states (e.g., ['hovered'], ['visual-focus']). |
hints |
Semantic hints for matching (parallels CSS .hint selectors). |
attributes |
Key-value attribute pairs for attribute selectors. |
ids |
Element IDs for ID selectors. |
debug |
Enable console group logging of selector matching results. |
UseUnionResult#
interface UseUnionResult {
styles: Record<string, StylePropertyGroup>;
current: StylePropertyGroup;
UnionProvider: ({ children }: { children: React.ReactNode }) => React.ReactElement;
}
| Return | Description |
|---|---|
styles |
Pre-computed StylePropertyGroup for all standard state combinations (base, hovered, pressed, checked, visual-focus, active-focus, disabled, highlighted, plus checked combos). |
current |
The StylePropertyGroup for the element's current state. |
UnionProvider |
A component wrapping UnionAncestorContext.Provider. Pushes this element into the ancestor chain for descendant useUnion() calls. |
useUnion()#
function useUnion(options: UseUnionOptions): UseUnionResult
The main hook. It:
- Reads
StyleRule[]fromUnionIRContext(set byUnionIRProviderat the app root). - Reads ancestor chain from
UnionAncestorContext(set by parentUnionProviders). - Pre-computes all standard state combinations via
resolveElement(). - Returns
{ styles, current, UnionProvider }.
If a UnionProvider is not rendered (leaf component), the hook still resolves styles correctly — it just doesn't extend the ancestor chain for any deeper components.
UnionIRProvider#
function UnionIRProvider({ rules, children }: { rules: StyleRule[]; children: React.ReactNode }): React.ReactElement
Root provider. Supplies the parsed CSS rules to all useUnion calls in the tree. Must be placed at the top of the application.
UnionIRContext / UnionAncestorContext#
UnionIRContext: React.Context<{ rules: StyleRule[] }> // style rules from theme
UnionAncestorContext: React.Context<{ elements: AncestorElementData[] }> // ancestor chain
unionIR (Vite Plugin)#
Now lives in @union-web/vite-plugin, re-exported from @union-web/react-plugin/vite.
import { unionIR } from '@union-web/react-plugin/vite';
// or directly:
import { unionIR } from '@union-web/vite-plugin/vite';
Build-time plugin that:
- Loads and parses defaults CSS, theme CSS, and
.colorsfile. - Resolves all variable expressions (
var(),custom-color(),modify-color(),mix()) usingKColorSchemeResolverandVariableResolver. - Provides a
virtual:union-irmodule exporting the resolvedStyleRule[]and CSS variable map. - Injects resolved CSS variables into HTML
<head>. - Watches input files for HMR.
interface UnionIROptions {
theme: string; // path to theme style.css
colors: string; // path to .colors file
defaults: string; // path to defaults CSS
}
Using UnionProvider#
Render a UnionProvider for any component that has descendants that will call useUnion():
function TextArea(props) {
const { current, UnionProvider } = useUnion({ role: 'textarea', state: [...] });
return (
<UnionProvider>
<textarea style={styleGroupToReact(current)} />
{!value && placeholder && <Placeholder text={placeholder} />}
</UnionProvider>
);
}
// Leaf — no UnionProvider needed
function Placeholder({ text }) {
const { current } = useUnion({ role: 'placeholder', hints: ['placeholder'] });
return <div style={styleGroupToReact(current)}>{text}</div>;
}
Debugging#
Pass debug: true to see selector matching in the browser console:
[useUnion] textarea (base) ancestors: [applicationwindow, page, columnlayout]
✓ textarea (spec: 0.1.0) { background-color: #2a2a2a; ... }
✓ > .placeholder (spec: 0.2.0) { color: #808080; }
result: { layout: ..., background: { color: "#2a2a2a" } }