Component Model
The Component Model is the Component Semantics Projection — layer 3 of the FSL architecture. It derives from the FSL Lexicon and FSL Structural Language and must not define vocabulary that contradicts them.
@ttoss/fsl-uiThis 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 dimension | Model name | Notes |
|---|---|---|
| Entity Kind | Entity | Values identical; field name is entity in ComponentMeta and all *Meta declarations |
| Structural Role | Structure | Root structural role of the component (e.g. root); legal values constrained per Entity via ENTITY_STRUCTURE |
| Composition Role | Composition | Flat vocabulary; Lexicon §4 values plus three declared profile extensions (step, summary, navigation). Per-Entity legality via ENTITY_COMPOSITION in taxonomy.ts. |
| Interaction Kind | — | Deferred per FSL §13.3 — not codified in this profile. See taxonomy.ts §Dimension Coverage for rationale and readmission criterion. |
| Evaluation | Evaluation | Values identical |
| Consequence | Consequence | Profile-narrowed subset — neutral, committing, destructive only (CONSEQUENCES in taxonomy.ts); the remaining Lexicon §6 values are rejected with recorded rationale (see Consequence below) |
| State | State | Values identical; runtime-resolved by React Aria render props, not authorially declared |
| Layer Role | — | Absorbed 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 Class | — | Deferred 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.
| Entity | Token ux context | Notes |
|---|---|---|
| Action | action | 1:1 |
| Input | input | 1:1 |
| Selection | input | Selection components consume input.* tokens; no separate selection UX context |
| Navigation | navigation | 1:1 |
| Feedback | feedback | 1:1 |
| Collection | informational | Collection surfaces consume informational.* for structural coloring |
| Overlay | informational | Overlay surfaces consume informational.* for surface coloring |
| Disclosure | navigation | Disclosure triggers colored as navigation.* when acting as location anchors |
| Structure | informational | Structural 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?).ComponentExpressionabove is the projection's conceptual shape;ComponentMetais 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.
| Entity | Use for | Typical examples |
|---|---|---|
| Action | triggering actions or commands | button, action button, icon button |
| Input | direct user input | text field, text area, search field |
| Selection | choosing one or more options | checkbox, radio group, select, picker |
| Collection | structured sets of items | menu, list, table, tree, grid |
| Navigation | movement across destinations or views | link, breadcrumbs, tabs, nav item |
| Disclosure | revealing or hiding related content in place | accordion, disclosure trigger |
| Overlay | temporary layered UI above the interface | dialog, popover, tooltip, drawer |
| Feedback | communicating state, status, or outcome | alert, banner, toast, progress |
| Structure | organizing interface structure and support surfaces | panel, 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).
| Role | Meaning | Legal Entities |
|---|---|---|
| primaryAction | main forward / commit action in a composite | Action |
| secondaryAction | subordinate but intentional action | Action |
| dismissAction | cancel / close without committing | Action |
| heading | compositional heading slot | Overlay, Structure |
| body | compositional body slot | Overlay, Structure |
| status | compositional status / validation slot | Input, Feedback |
| control | primary control-bearing slot | Input, Selection |
| label | naming / label slot | Input, Selection, Structure |
| description | descriptive / helper-text slot | Input, Selection, Structure |
| supporting | supporting child slot (broader than label/description) | Input, Structure |
| selection | selection-bearing slot | Selection |
| step | step slot (e.g. progression marker) | Structure |
| summary | summary slot | Structure |
| navigation | navigation slot inside a structural composite | Structure |
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-parton the composite container identify the parent (e.g.data-scope="dialog" data-part="actions"onDialogActions).data-compositionon 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:
| Value | Use for | Legal Entities |
|---|---|---|
| primary | main intended emphasis | Action, Collection, Overlay, Navigation, Disclosure, Feedback, Structure |
| secondary | subordinate but still intentional | Action, Overlay, Navigation |
| accent | deliberately differentiated emphasis | Action, Overlay, Navigation, Feedback |
| muted | de-emphasized but still meaningful | Action, Collection, Overlay, Navigation, Disclosure, Structure |
| positive | affirming, successful, or favorable | Feedback |
| caution | warning or careful-attention signal | Feedback |
| negative | harmful, erroneous, or adverse | Action, 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
onRetryprop), not inComponentMeta. - safeDefaultRequired is a derived policy:
destructivealready 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.
| Value | Use for |
|---|---|
| neutral | no special risk profile |
| committing | moves to a more committed state |
| destructive | causes 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:
| State | Meaning |
|---|---|
| default | No special condition active |
| hover | Pointer is over the element |
| active | Element is being activated / pressed |
| focused | Element has keyboard focus |
| disabled | Element is not interactive |
| selected | Item is selected within a collection |
| pressed | Toggle is in its on-press state |
| checked | Checkbox-like element is checked |
| indeterminate | Mixed or partial selection state |
| expanded | Disclosure or select is open |
| current | Navigation item matches the current location |
| visited | Link has been previously visited (CSS-only — browsers hide :visited from JS; see below) |
| droptarget | Element is a valid target for a drag operation |
| invalid | Control'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.
Button→ActionSearchField→InputMenu→CollectionDialog→Overlay
2. Add Composition when the instance occupies a slot in a composite
Buttonin a dialog footer →composition: 'primaryAction'Buttonas 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.
Dialog footer buttons
// 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
- Entity is always primary. It defines the component, not the instance.
- Composition names a slot, not the component. Composition never replaces Entity.
- Composition legality is per Entity. A role is only valid on the Entities listed in the Composition roles table (source:
ENTITY_COMPOSITION). - Evaluation is semantic, not visual. Choose it for its meaning, not its color.
- Consequence is about outcome risk. It shapes interaction policy before styling.
- 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.