Skip to main content

Component Model

The Component Model is the Component Semantics Projectionlayer 3 of the FSL architecture. It derives from the FSL Lexicon and FSL Structural Language and must not define vocabulary that contradicts them.

Status: implemented in @ttoss/fsl-ui

This document is the design specification for the Component Semantics Projection, now implemented by @ttoss/fsl-ui: taxonomy.ts (vocabulary + legality matrices), ComponentMeta, ENTITY_COMPOSITION / ENTITY_STRUCTURE / ENTITY_TOKEN_MAPPING (packages/fsl-ui/src/tokens/projection.ts), and contract tests that auto-validate every component against the matrices. The Semantic Token Projection (layer 4, @ttoss/fsl-theme) is also implemented. The Resolution contract (layer 5) is satisfied by distributed mechanisms — see the FSL overview. Where this document and the shipped code diverge, the code + its contract tests are the source of truth.

The central rule:

A component has an immutable identity. An instance carries that identity into a composition.

FSL dimension mapping

The model adopts FSL dimension names directly (no projection renames; FSL §17.1 permits renames but this profile keeps the foundation vocabulary):

FSL dimensionModel nameNotes
Entity KindEntityValues identical; field name is entity in ComponentMeta and all *Meta declarations
Structural RoleStructureRoot structural role of the component (e.g. root); legal values constrained per Entity via ENTITY_STRUCTURE
Composition RoleCompositionFlat vocabulary; Lexicon §4 values plus three declared profile extensions (step, summary, navigation). Per-Entity legality via ENTITY_COMPOSITION in taxonomy.ts.
Interaction KindDeferred per FSL §13.3 — not codified in this profile. See taxonomy.ts §Dimension Coverage for rationale and readmission criterion.
EvaluationEvaluationValues identical
ConsequenceConsequenceProfile-narrowed subset — neutral, committing, destructive only (CONSEQUENCES in taxonomy.ts); the remaining Lexicon §6 values are rejected with recorded rationale (see Consequence below)
StateStateValues identical; runtime-resolved by React Aria render props, not authorially declared
Layer RoleAbsorbed per FSL §13.3 — captured by the token projection's surfaceType (control/surface), the elevation strata (flat/raised/overlay/blocking), and the z-index layer scale (base/sticky/overlay/blocking/transient), which jointly recover all six Lexicon §8 layer roles. See taxonomy.ts §Dimension Coverage.
Context ClassDeferred per FSL §13.3 — refinement dimension (density, mode, a11y preferences) with no prototype exercising it. Readmission criterion: a component that dispatches on a context class at runtime — e.g. a density variant that changes which spacing/sizing tokens a component consumes. Mode switching shipped end-to-end in @ttoss/fsl-theme without any component-level dispatch, evidence that mode lives at the theme layer, not in ComponentMeta. See taxonomy.ts §Dimension Coverage.

Entity → Token UX context mapping

The normative Entity → ux context mapping lives in ENTITY_TOKEN_MAPPING in packages/fsl-ui/src/tokens/projection.ts — the single source of truth, enforced by contract tests. The table below mirrors it for reading; on any divergence, the code wins.

EntityToken ux contextNotes
Actionaction1:1
Inputinput1:1
SelectioninputSelection components consume input.* tokens; no separate selection UX context
Navigationnavigation1:1
Feedbackfeedback1:1
CollectioninformationalCollection surfaces consume informational.* for structural coloring
OverlayinformationalOverlay surfaces consume informational.* for surface coloring
DisclosurenavigationDisclosure triggers colored as navigation.* when acting as location anchors
StructureinformationalStructural surfaces consume informational.*

For the full ux role and state grammar, see the Colors family — FSL Entity Kind Mapping.

ComponentExpression

The model is expressed as a ComponentExpression — the typed semantic expression that the resolution pipeline consumes:

type ComponentExpression = {
entity: Entity; // required — what the component IS
structure: StructuralRole; // required — root structural role (e.g. 'root'); legality per Entity via ENTITY_STRUCTURE
composition?: CompositionRole; // optional — flat slot name (FSL Lexicon §4)
evaluation?: Evaluation; // optional — emphatic meaning
consequence?: Consequence; // optional — risk profile
};

All dimensions are defined in taxonomy.ts and derived from FSL core vocabulary.

Code type: the implementation exposes ComponentMeta<E> (packages/fsl-ui/src/semantics/componentMeta.ts) — the identity type every component declares (entity, structure, composition?, consequence?). ComponentExpression above is the projection's conceptual shape; ComponentMeta is its shipped runtime surface.


Entity

Entity answers: What is this component?

Every component has exactly one Entity. It cannot change based on context, variant, or usage. If a different Entity is required, a different component must exist.

EntityUse forTypical examples
Actiontriggering actions or commandsbutton, action button, icon button
Inputdirect user inputtext field, text area, search field
Selectionchoosing one or more optionscheckbox, radio group, select, picker
Collectionstructured sets of itemsmenu, list, table, tree, grid
Navigationmovement across destinations or viewslink, breadcrumbs, tabs, nav item
Disclosurerevealing or hiding related content in placeaccordion, disclosure trigger
Overlaytemporary layered UI above the interfacedialog, popover, tooltip, drawer
Feedbackcommunicating state, status, or outcomealert, banner, toast, progress
Structureorganizing interface structure and support surfacespanel, section, shell, frame

Composition Model

Composition answers: What slot does this instance occupy inside a larger composite?

Composition is a flat vocabulary per FSL Lexicon §4 and FSL §5.4. A composition role names the slot; legality is per Entity (not per parent component). When omitted, the component resolves tokens from its Entity default. The vocabulary is the Lexicon §4 set plus three profile extensions (step, summary, navigation — Structure-only slots), declared per FSL §17's extension model.

Case discipline: the navigation slot (lowercase, Composition) is distinct from the Navigation Entity Kind — the same convention the Lexicon applies to Structure/structure (§10.12) and Overlay/overlay (§10.13). The slot names a position inside a structural composite; the Entity names what a component is.

Composition roles

The projection codifies 14 composition roles. The table shows each role's meaning and which Entities may carry it (source of truth: ENTITY_COMPOSITION in taxonomy.ts).

RoleMeaningLegal Entities
primaryActionmain forward / commit action in a compositeAction
secondaryActionsubordinate but intentional actionAction
dismissActioncancel / close without committingAction
headingcompositional heading slotOverlay, Structure
bodycompositional body slotOverlay, Structure
statuscompositional status / validation slotInput, Feedback
controlprimary control-bearing slotInput, Selection
labelnaming / label slotInput, Selection, Structure
descriptiondescriptive / helper-text slotInput, Selection, Structure
supportingsupporting child slot (broader than label/description)Input, Structure
selectionselection-bearing slotSelection
stepstep slot (e.g. progression marker)Structure
summarysummary slotStructure
navigationnavigation slot inside a structural compositeStructure

Parent disambiguation

Because the vocabulary is flat, the same role name may appear in multiple composites — for example, a label slot exists on both TextField (Input) and a Structure composite. Runtime and CSS disambiguation comes from the rendered DOM, not from adding a host level to the data model:

  • data-scope + data-part on the composite container identify the parent (e.g. data-scope="dialog" data-part="actions" on DialogActions).
  • data-composition on the slot-bearing child carries the role name.

Example selector: [data-scope="dialog"][data-part="actions"] [data-composition="primaryAction"] resolves a dialog's primary action unambiguously, without a host level.


Evaluation

Evaluation answers: What emphatic or evaluative meaning does this expression carry?

Evaluation is optional. When omitted, each component applies its own documented default — and that is the design: defaults live with the component, because that is where the knowledge lives. Add it explicitly only when the default is wrong.

Legality is per Entity (source of truth: ENTITY_EVALUATION in taxonomy.ts), same as the Composition table:

ValueUse forLegal Entities
primarymain intended emphasisAction, Collection, Overlay, Navigation, Disclosure, Feedback, Structure
secondarysubordinate but still intentionalAction, Overlay, Navigation
accentdeliberately differentiated emphasisAction, Overlay, Navigation, Feedback
mutedde-emphasized but still meaningfulAction, Collection, Overlay, Navigation, Disclosure, Structure
positiveaffirming, successful, or favorableFeedback
cautionwarning or careful-attention signalFeedback
negativeharmful, erroneous, or adverseAction, Overlay, Feedback

Input and Selection carry no evaluations: form controls are data-entry surfaces, not decision hierarchies, and validation is the runtime invalid State (isInvalid), never evaluation: 'negative' — see the design note on ENTITY_EVALUATION in taxonomy.ts.


Consequence

Consequence answers: What user-facing consequence or risk profile does this carry?

Consequence is optional. When omitted, neutral is implied. Distinct from Evaluation: negative is evaluative meaning; destructive is outcome risk — both may appear simultaneously.

The profile codifies three values (CONSEQUENCES in taxonomy.ts) — a deliberate narrowing of the Lexicon §6 vocabulary, declared per FSL §13.3. The remaining Lexicon terms are rejected with the following rationale:

  • reversible is the logical complement of committing; carrying both doubles the vocabulary without adding an expressible distinction.
  • interruptive is absorbed by the Entity Overlay — an Overlay is interruptive by kind, and non-overlay interruption has no component prototype to justify separate vocabulary.
  • recoverable describes a runtime outcome of failure, not an authorial meta; recovery support belongs in component API (e.g. an onRetry prop), not in ComponentMeta.
  • safeDefaultRequired is a derived policy: destructive already implies the need for a safe default, so codifying the policy separately would create a second source of truth for the same constraint.

The rejections are invariants of this profile, not of FSL — a different profile may codify more of the vocabulary if its component prototypes create new distinctions.

ValueUse for
neutralno special risk profile
committingmoves to a more committed state
destructivecauses deletion or materially harmful loss

Only Action carries consequence — every other entity's legal set in ENTITY_CONSEQUENCE is empty.


Interaction

Interaction Kind is a FSL foundational dimension (FSL Lexicon §3, FSL §5.3) that this profile does not currently codify. The disposition is Deferred per FSL §13.3.

Readmission requires a component that dispatches behaviour on Interaction Kind at runtime — for example, a Wizard that progresses on navigate.step versus a Link that follows navigate.link. Until such a prototype exists, the dimension carries no expressible distinction in ComponentMeta. See taxonomy.ts §Dimension Coverage for the full rationale.


State

State answers: What interactional or semantic condition is currently active?

State is not a prop passed at the expression level — it is runtime-resolved by React Aria render props (isHovered, isFocused, isPressed, isSelected, …) and surfaced as the CSS selector layer:

StateMeaning
defaultNo special condition active
hoverPointer is over the element
activeElement is being activated / pressed
focusedElement has keyboard focus
disabledElement is not interactive
selectedItem is selected within a collection
pressedToggle is in its on-press state
checkedCheckbox-like element is checked
indeterminateMixed or partial selection state
expandedDisclosure or select is open
currentNavigation item matches the current location
visitedLink has been previously visited (CSS-only — browsers hide :visited from JS; see below)
droptargetElement is a valid target for a drag operation
invalidControl's value failed validation (runtime — isInvalid, never authorial; Lexicon §10.15)

Not all states are meaningful for every Entity — checked is only surfaced by selection components. Legality here is React Aria's runtime concern (it only emits the render-prop for applicable primitives), not a build-time matrix — the declared legality source per FSL Structural Language §10.1 is runtime resolution — with one exception: visited cannot be runtime-resolved at all. Browsers hide :visited state from JavaScript for privacy, so no library can emit the flag; its legality source is structural impossibility (§10.1), declared below.

Concurrent states and resolution order

React Aria may report several state flags simultaneously (an item can be selected, focused, and hovered at once). The profile resolves them through STATE_PRIORITY (taxonomy.ts) — a declared, deterministic total order, as FSL Structural Language §11.4 requires. isSelected resolves context-aware: the theme declares selected where a ux context means membership in a set (navigation, informational) and checked where a control is two-state (the input context), so the cascade consults the token set per colour dimension with an explicit fallback — checked when the set declares it, then selected when the set declares it, then the normal miss to default (fsl-ui ADR-044). The transient isPressed flag deliberately resolves to active — a semantic reservation, not a theme gap: the pressed token state means the persistent toggle-on, which ToggleButton reads by mapping its isSelected to pressed inline (fsl-ui ADR-042) and which the theme ships divergent from active on purpose; the collapse keeps the persistent state reserved for toggle semantics. Readmission criterion: a component whose transient press must paint differently from active at the token level. Two vocabulary states have no render-prop flag, for different reasons: droptarget because no component surfaces a drag target yet, and visited because no flag can ever exist — browsers hide :visited from JS for privacy, making it a structural impossibility per §10.1 rather than a pending flag; its tokens are spent through the CSS pseudo-class, outside the cascade. Per §11.4 these resolutions and absences are recorded here rather than silent.


How to use the model

1. Set Entity

Classify the component itself.

  • ButtonAction
  • SearchFieldInput
  • MenuCollection
  • DialogOverlay

2. Add Composition when the instance occupies a slot in a composite

  • Button in a dialog footer → composition: 'primaryAction'
  • Button as a form submit → composition: 'primaryAction'
  • TextField validation message → composition: 'status'
  • Checkbox inside a RadioGroup-like set → composition: 'selection'

Legal values depend on the component's Entity — see the Composition roles table above.

3. Add Evaluation when the default inference isn't right

Most expressions don't need explicit Evaluation. Add it when the standard inference is wrong:

  • Destructive confirm button → evaluation: 'negative'
  • Success state feedback → evaluation: 'positive'
  • Subdued ghost action → evaluation: 'muted'

4. Add Consequence when the interaction carries a material risk profile

  • Delete / irreversible action → consequence: 'destructive'
  • Save with no undo → consequence: 'committing'

Usage Examples

Token paths reference the Semantic Token Projection (layer 4) and are confirmed against the shipped @ttoss/fsl-theme.

// Save
{ entity: 'Action', composition: 'primaryAction' }
// → action.primary.background.default, action.primary.text.default

// Back to editing
{ entity: 'Action', composition: 'secondaryAction' }
// → action.secondary.background.default, action.secondary.text.default

// Cancel
{ entity: 'Action', composition: 'dismissAction' }
// → action.muted.text.default

TextField (Input composite)

// Main control
{ entity: 'Input', composition: 'control' }
// → input.primary.background.default, input.primary.border.default, input.primary.text.default

// Label
{ entity: 'Input', composition: 'label' }
// → input.primary.text.default

// Helper text
{ entity: 'Input', composition: 'description' }
// → input.muted.text.default

// Validation message
{ entity: 'Input', composition: 'status' }
// → input.negative.text.default

Dialog (Overlay composite)

// Heading
{ entity: 'Overlay', composition: 'heading' }
// → informational.primary.text.default

// Body
{ entity: 'Overlay', composition: 'body' }
// → informational.primary.text.default

Form (Structure composite)

// Actions row container
{ entity: 'Structure', composition: 'supporting' }
// → informational.muted.background.default

// Submit button inside the actions row
{ entity: 'Action', composition: 'primaryAction' }
// → action.primary.background.default

Destructive action

{
entity: 'Action',
composition: 'primaryAction',
evaluation: 'negative',
consequence: 'destructive',
}
// → action.negative.background.default, action.negative.text.default
// A `destructive` consequence authorises downstream safe-default treatment of the cancel path.

Rules

  1. Entity is always primary. It defines the component, not the instance.
  2. Composition names a slot, not the component. Composition never replaces Entity.
  3. Composition legality is per Entity. A role is only valid on the Entities listed in the Composition roles table (source: ENTITY_COMPOSITION).
  4. Evaluation is semantic, not visual. Choose it for its meaning, not its color.
  5. Consequence is about outcome risk. It shapes interaction policy before styling.
  6. Keep the model small. New values must come from the FSL foundation or be added through FSL governance.

Implementation: See UI Components for how this model is realized in React.