ADR-007 Section 6 defines the extensibility contract requiring slotProps support for named component parts. However, Mitosis (the multi-target component compiler) cannot compile dynamic object spread syntax ({...props.slotProps?.root}) on JSX elements.
This is a deliberate Mitosis limitation because several output targets (Stencil, Vue, Angular) do not support arbitrary dynamic attribute spread. Attempting to use spread results in compilation errors or runtime failures in those targets.
The challenge: how do we implement the slotProps extensibility contract (ADR-007 Section 6) within Mitosis constraints while maintaining:
- Cross-platform compatibility (React, Vue, Stencil, React Native, Flutter)
- Type safety
- Runtime flexibility (metadata-driven UI can provide slotProps at render time)
- No build-time magic or code generation
Use Runtime Part Resolution: pure function helpers that components call per-element to read individual values from slotProps, avoiding dynamic spread entirely.
Three pure functions provide the slotProps mechanism:
export type SlotPropsMap = Partial<Record<string, Record<string, unknown>>> | undefined;
export function partClass(
slotProps: SlotPropsMap,
partName: string,
baseClass: string,
): string {
if (!slotProps || typeof slotProps !== 'object') return baseClass;
const partMap = slotProps[partName];
if (!partMap) return baseClass;
const extra = partMap.className;
if (extra === undefined || extra === null || extra === '') return baseClass;
if (typeof extra !== 'string') return baseClass;
if (!baseClass) return extra;
return baseClass + ' ' + extra;
}
export function partAttr(
slotProps: SlotPropsMap,
partName: string,
attrName: string,
): unknown {
if (!slotProps || typeof slotProps !== 'object') return undefined;
const partMap = slotProps[partName];
if (!partMap) return undefined;
return partMap[attrName];
}
export function partStyle(
slotProps: SlotPropsMap,
partName: string = 'root',
): unknown {
if (!slotProps || typeof slotProps !== 'object') return undefined;
const partMap = slotProps[partName];
if (!partMap) return undefined;
return partMap.style;
}import { partClass, partAttr } from '@/core/part-props';
export default function BcInput(props: BcInputProps) {
return (
<div
class={partClass(props.slotProps, 'root', 'flex flex-col gap-1')}
data-part="root"
data-testid={partAttr(props.slotProps, 'root', 'data-testid') as string}
>
<label
class={partClass(props.slotProps, 'label', 'text-sm font-medium')}
data-part="label"
>
{props.label}
</label>
<input
class={partClass(props.slotProps, 'input', 'border rounded px-2 py-1')}
data-part="input"
aria-label={partAttr(props.slotProps, 'input', 'aria-label') as string}
/>
</div>
);
}Property access and string concatenation are valid Mitosis JSX:
- ✅
props.slotProps?.root?.className— property access - ✅
baseClass + ' ' + extra— string concatenation - ❌
{...props.slotProps?.root}— dynamic spread (not supported)
Pure functions have no DOM dependency:
- React/Vue/Svelte: Direct function calls
- React Native: Same function calls, NativeWind classes
- Flutter/Dart: Port the same logic to Dart
- Stencil/Web Components: Works in shadow DOM
Components call helpers at render time, so:
- Metadata-driven UI (ADR-005) can provide
slotPropsfrom schema data - No build-time code generation required
- Dynamic
slotPropsfrom parent components work naturally
SlotPropsMaptype alias for better IDE support- Type guards validate inputs before use (never throw)
- Return safe defaults for invalid inputs
slotProps.root.className is concatenated after the component's base classes, preserving token-driven styling as the source of truth (ADR-002 Section 3, ADR-006 Section 4).
Example:
// Component base: "flex items-center bg-background"
// Consumer slotProps: { root: { className: "p-4 shadow-lg" } }
// Result: "flex items-center bg-background p-4 shadow-lg"Rationale: Consumer classes append, never override the base. This prevents accidental removal of critical styling (e.g., flex, items-center).
Components wire slotProps passthrough for:
className— styling overridesdata-*attributes — testing, analyticsaria-*attributes — a11y overridesstyle— inline style (where supported)
Event handler passthrough is intentionally excluded to keep the component surface controlled.
Every element that accepts slotProps must emit data-part="{partName}":
<div class={partClass(props.slotProps, 'root', baseClass)} data-part="root">Rationale: Consumers can target parts via CSS selectors and testing tools, independent of slotProps:
[data-part="root"] { margin-bottom: 1rem; }
[data-part="label"] { font-weight: 600; }Components wire only the attributes they explicitly support per part. This is a feature, not a limitation:
- Enterprise UI libraries should have a controlled surface
- Arbitrary prop passthrough invites breakage across framework targets
- Clear which attributes are supported per part
- Easier to audit and maintain
Trade-off: More verbose component code, but safer and more predictable.
partClass and partAttr are pure functions with no DOM dependency. A Dart port can implement the same contract:
String partClass(
Map<String, Map<String, dynamic>>? slotProps,
String partName,
String baseClass,
) {
if (slotProps == null) return baseClass;
final partMap = slotProps[partName];
if (partMap == null) return baseClass;
final extra = partMap['className'];
if (extra == null || extra.isEmpty) return baseClass;
if (extra is! String) return baseClass;
if (baseClass.isEmpty) return extra;
return '$baseClass $extra';
}Flutter adapters receive slotProps as a typed map and apply values through the equivalent Dart widget API.
BcRenderer (ADR-005) may populate slotProps from UiSchema metadata at render time:
// Schema field with density: 'compact'
const slotProps = {
root: { className: 'p-1' },
input: { className: 'text-xs' },
};
<BcInput slotProps={slotProps} />This keeps the renderer a simple compositor without per-component knowledge.
- Works across all targets — React, Vue, Stencil, React Native, Flutter
- Type-safe — Type guards prevent runtime errors
- No build magic — Pure runtime functions, no code generation
- Metadata-driven friendly —
BcRenderercan provideslotPropsfrom schema - Additive styling — Token-driven classes remain source of truth
- Testable — Pure functions are easy to unit test (34 tests in
part-props.test.ts, plus 3unsubscribemethod tests instore.test.tsadded 2026-06-21) - Portable — No DOM dependency, works in SSR/React Native
- Explicit wiring required — Each component must call
partClass/partAttrper element - More verbose — Cannot use generic spread, must wire each supported attribute
- Discipline required — Authors must remember to wire all supported parts
For enterprise UI libraries, explicit wiring is preferable to generic spread because:
- Controlled surface prevents arbitrary prop passthrough
- Clear which attributes are supported per part
- Easier to audit and maintain
- Works across all framework targets
The verbosity cost is acceptable for the safety and portability benefits.
All criteria met as of 2026-06-18:
- All 25 components wired — Verified by
runtime-part-rollout.test.ts -
partClassis additive (never replaces) — Tested inpart-props.test.ts -
data-partattributes emitted — All components emitdata-part="{partName}" - Type-safe
slotPropsinterface —SlotPropsMaptype with type guards - Works in SSR/React Native — No DOM dependency, guarded
typeofchecks - Test coverage — 34 unit tests + 18 runtime tests, 100% passing
- Documented in ADR-007 Section 6.1 — Cross-referenced
packages/ui/src/core/part-props.test.ts— 34 unit testspackages/ui/src/core/reactive/store.test.ts— 3unsubscribemethod tests (added 2026-06-21, BC-024 fix)packages/ui/src/core/part-props.runtime.test.ts— 18 runtime behavior testspackages/ui/src/components/runtime-part-rollout.test.ts— Verifies all 25 components wired
All 25 Core Integration Surface components use Runtime Part Resolution:
Form Controls (10): BcInput, BcSelect, BcCombobox, BcCheckbox, BcToggle, BcSegmented, BcNumberField, BcDateField, BcTimeField, BcMultiSelect
Forms (2): BcForm, BcReactiveScope
Overlays (8): BcDialog, BcAlertDialog, BcDrawer, BcPopover, BcTooltip, BcDropdown, BcContextMenu, BcCommand
Navigation/Layout (4): BcTabs, BcAccordion, BcDisclosure, BcSplitter
Data (1): BcDataTable
- ADR-007: Enterprise Universal UI API Contract — Defines slotProps extensibility contract (Section 6)
- ADR-008: Contract-First API Freeze — Enforces contract matrix including slotProps
- ADR-010: Web Component Event Bridge — Complementary mechanism for CustomEvent dispatch
As of 2026-06-21, partAttr wiring has been applied across 30 components
(BC-002 fix). Every JSX element with data-part="X" now emits:
data-testid={partAttr(props.slotProps, 'X', 'data-testid') as string}Input/button/label elements with data-part also wire:
aria-label={partAttr(props.slotProps, 'X', 'aria-label') as string}Components wired: BcInput, BcDisclosure, BcDataTable, BcCheckbox, BcCombobox, BcDateField, BcFieldDuration, BcFieldDynLink, BcLookupModal, BcFieldMorph, BcMultiSelect, BcNumberField, BcSegmented, BcSelect, BcFieldTableSelect, BcTimeField, BcToggle, BcAccordion, BcSplitter, BcTabs, BcDropdown, BcAlertDialog, BcCommand, BcContextMenu, BcDialog, BcDrawer, BcPopover, BcTooltip, BcForm, BcList.
Verification: Playwright E2E test bc-002-partattr.spec.ts confirms
data-testid from slotProps reaches the DOM in a real browser.
- Implementation:
packages/ui/src/core/part-props.ts - Mitosis documentation: JSX constraints, property access, string concatenation
- ADR-007 Section 6.1: Runtime Part Resolution specification
- Test suite:
packages/ui/src/core/part-props.test.ts,part-props.runtime.test.ts