Mixins / Inside-out composition
A small example
The component author publishes slots:
const FieldSlots = Slots.define({
root: Slot.make({ capability: Capability.Container }),
input: Slot.make({
capability: Capability.TextInput,
events: [Event.Input],
attributes: [Attr.AriaInvalid],
}),
})A design system can style those points without editing the view:
const FieldStyle = Style.forSlots(FieldSlots)({
root: Style.class('field'),
input: Style.class('field-input'),
})A feature can independently attach behavior:
const Validation = Behavior.forSlots(FieldSlots)<FieldInput, Message>({
input: Behavior.slot({
requires: { capability: Capability.TextInput, attributes: [Attr.AriaInvalid] },
attributes: ({ input, h }) => [h.AriaInvalid(input.invalid)],
}),
})And the view resolves both through the same published points:
const Field = SlotView.forMessages<Message>()
.define(FieldSlots, (input: FieldInput, slots, h) =>
h.label(slots.root.attrs(), [
h.input(
slots.input.attrs([
h.Value(input.value),
h.OnInput(value => Message.ChangedValue({ value })),
]),
),
]),
)
.pipe(Style.attach(FieldStyle), Behavior.attach(Validation))The important part is not the amount of code. It is where responsibility lives:
component author → publishes stable extension points
style author → owns appearance
feature author → owns element-level behavior
Foldkit app → still owns state and transitions
resolver → owns merge/conflict policyConflict rules are part of the abstraction
Attachments are not concatenated and left to incidental renderer ordering. The resolver has one merge policy:
- classes are additive and deduplicated;
- inline styles merge per property;
- an event or scalar attribute has one owner;
- a second owner produces a stable diagnostic instead of silently winning;
ChildAttributevalues are preserved by identity;KeyandInnerHTMLare structural and cannot be overridden;- multiple mounts compose into one
OnMount.
This is what lets a design system and a feature attach independently without each one needing to know what the other did.
Slots are contracts, not DOM nodes
A slot is metadata about an extension point. It may declare:
- a Capability such as
Container,InteractiveorTextInput; - published Event and Attr tokens;
- platform Requirement tokens such as Keyboard, Pointer or Clipboard;
hiddenpoints that are internal to the component;protectedevents, attributes or style properties that attachments may not replace.
Capability is hierarchical, so a Behavior requiring Interactive can attach to a TextInput.
Custom capabilities, events and attributes can be created with their corresponding .make
constructors.