frontend-patterns/history/260927-ComplexWebAppUiComponents_chatgpt.txt
tegwick eaa1608771 Draft CAND-0018 Capability Contract Boundary from a screened AI-chat lead
history/260927-ComplexWebAppUiComponents_chatgpt.txt is a pasted AI-chat
transcript on UI-integration architecture. It fails this repo's primary-source
bar (unauthored, unverifiable broken citation markers), so it is screened as
a leads-only document, not cataloged as a source, and never cited as evidence.

Instead read the two real specs it pointed to: MDN Custom Elements (SRC-0046)
and webpack Module Federation (SRC-0047). Neither defines a semantic UI
capability contract (inputs/events/commands/slots) as a first-class concept;
that gap motivates the new draft hypothesis, distinct from the existing
Independent Feature Boundary (release cadence) and Vertical Feature Slice
(source grouping) candidates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 290719@bnt-lap001
Assistant-Session: 55cbe504-1bfe-4932-b761-29016523c87b
2026-09-27 23:29:10 +02:00

480 lines
No EOL
13 KiB
Text
Executable file

## Prompt
Is there a best practice for providing ui components to complex web application ui environments integrating multiple user interfaces and interface components?
## Exploration
Yes. For a complex web application that needs to integrate multiple user interfaces, teams, frameworks, or independently evolving components, there is a fairly mature set of practices—but not one single pattern. The useful way to think about it is as an **integration architecture with several layers**, rather than merely as a component library.
A good default architecture today looks roughly like this:
```text
Application Shell
│
├── UX / Design System
│ ├── design tokens
│ ├── primitive components
│ └── interaction conventions
│
├── UI Integration Contract
│ ├── inputs / properties
│ ├── outputs / events
│ ├── commands
│ ├── lifecycle
│ └── capability metadata
│
├── Application Components
│ ├── search-ui
│ ├── customer-editor
│ ├── document-viewer
│ └── reporting-ui
│
├── Composition Layer
│ ├── routing
│ ├── layout
│ ├── authentication context
│ ├── shared state
│ └── event/message bus
│
└── Runtime Integration
├── ordinary packages
├── Web Components
├── module federation
├── micro-frontends
└── iframe/process isolation when necessary
```
The important distinction is that these layers solve **different problems**.
### 1. Use a design system for visual and interaction coherence
A design system should define things such as typography, spacing, colors, accessibility conventions, buttons, dialogs, forms, navigation, tables, notifications, etc.
But a design system alone is not enough. It tells you that all applications should use the same sort of `<Button>`; it does not tell the host application how a `CustomerEditor` announces that a customer was saved.
I would therefore keep **design-system contracts** separate from **application-component contracts**.
---
### 2. Prefer framework-neutral contracts at integration boundaries
Inside an application, React-to-React or Vue-to-Vue component APIs are perfectly reasonable.
Across independently developed systems, however, tying the integration contract to:
```tsx
<CustomerEditor
customer={customer}
onSaved={...}
/>
```
creates fairly strong coupling to React, its version, its rendering model, and often its state-management conventions.
A more durable boundary is something conceptually like:
```html
<customer-editor customer-id="4711">
</customer-editor>
```
with:
```text
attributes / properties -> configuration and input
DOM events -> observations/output
methods -> explicit commands
slots -> host-provided presentation
CSS custom properties -> theming
```
That is exactly the architectural territory that **Web Components** were designed for. Custom Elements give you browser-native component boundaries, while Shadow DOM can provide DOM/CSS isolation. :chatgpt-content-reference{index="0"}
This doesn't imply that you have to write applications without React, Vue, Svelte, Lit, etc. A Web Component can simply be the **published interoperability surface** around an implementation written using another technology.
---
### 3. Treat coarse-grained UI capabilities differently from widgets
There is an important granularity boundary.
Things such as:
```text
Button
Checkbox
DatePicker
Popover
DataGrid
```
are design-system components.
Things such as:
```text
CustomerSearch
InvoiceEditor
DocumentViewer
UserAdministration
OrderWorkbench
```
are better thought of as **UI capabilities**.
The latter should have explicit contracts and should often be independently versionable.
For example:
```yaml
component: customer-editor
version: 2.3
requires:
- identity.user
- customer.read
- customer.write
inputs:
customerId: string
mode: [view, edit]
events:
- customer-selected
- customer-updated
- editing-cancelled
commands:
- reload
- focus
- reset
slots:
- toolbar
- footer
context:
- locale
- theme
- tenant
```
This starts to look less like a component API and more like a **small interface definition language for UI capabilities**. For large systems, I think this is the more useful abstraction.
---
### 4. Separate *composition* from *implementation*
A common architectural mistake is letting every component know how the application fits together.
Instead:
```text
Component:
"I can edit a customer."
Shell:
"You appear here,
for this tenant,
with this identity,
at this route,
beside these other capabilities."
```
The application shell should generally own:
- routing
- navigation
- page layout
- authentication/session context
- authorization decisions
- locale
- theme
- notifications
- observability
- feature flags
- capability discovery
- failure boundaries
Individual UI capabilities should own their domain interaction.
This is very similar to the distinction between a **control plane** and an **execution plane**.
---
### 5. Use events rather than shared mutable state across boundaries
Within one frontend, a Redux/Zustand/Pinia/etc. store can be useful.
Across independently evolving UI components, a giant shared store tends to become an implicit API.
Instead of:
```text
component A
↓
global state object
↑
component B
```
prefer:
```text
CustomerSearch
│
│ customer-selected
↓
Application Shell
│
│ selectedCustomer=123
↓
CustomerEditor
```
or an event/message mechanism where appropriate.
The semantic distinction is useful:
```text
properties desired state/configuration
events things that happened
commands explicit requests
queries information requests
```
That simple vocabulary dramatically improves UI contracts.
---
### 6. Use isolation deliberately rather than universally
There is a spectrum:
```text
Shared source/component
↓
npm package
↓
Web Component
↓
runtime remote module
↓
micro-frontend
↓
iframe
```
Each step provides more autonomy/isolation, but also introduces more integration cost.
**Web Components** are particularly attractive for reusable framework-independent components because Shadow DOM can prevent CSS and DOM internals from colliding with their host application. :chatgpt-content-reference{index="1"}
For larger independently deployed application fragments, **Module Federation** is another established technique. Webpack explicitly describes its purpose as allowing separate builds to expose and consume modules at runtime, including the micro-frontend use case. :chatgpt-content-reference{index="2"}
I would not make either the universal solution.
A useful rule is:
> Use the weakest isolation mechanism that provides sufficient organizational and technical independence.
---
### 7. Avoid the "micro-frontends everywhere" trap
Micro-frontends are useful when there is genuinely independent ownership:
```text
Team A Team B Team C
Billing CRM Analytics
release A release B release C
\ | /
Application Shell
```
They are much less attractive when one team owns everything. You otherwise pay for:
- duplicate dependencies
- runtime orchestration
- version incompatibilities
- inconsistent UX
- authentication propagation
- routing complexity
- performance overhead
- distributed debugging
without getting much organizational independence in return.
---
### 8. Make context explicit
One pattern I particularly recommend for complex platforms is a small, stable **host context**:
```ts
interface UIHostContext {
identity
tenant
locale
theme
permissions
navigation
notifications
telemetry
}
```
But don't expose an enormous application object.
Instead expose capabilities:
```text
context.identity.currentUser()
context.navigation.open(...)
context.notifications.publish(...)
context.telemetry.record(...)
```
This gives embedded components access to platform services without allowing them to become coupled to the entire host implementation.
---
### 9. Explicitly distinguish several kinds of compatibility
A UI component can be technically loadable and nevertheless incompatible.
I would track at least:
```text
Protocol compatibility
Can host and component communicate?
Semantic compatibility
Do "customer-selected" and "customerId" mean the same thing?
Visual compatibility
Does it fit the design system?
Interaction compatibility
Keyboard, focus, navigation, modal behavior, etc.
Security compatibility
Does it respect identity/authorization boundaries?
Lifecycle compatibility
Can it mount, unmount, reconnect and upgrade safely?
Operational compatibility
Can it be observed, debugged and degraded independently?
```
This becomes important once components are reused across several applications.
---
## The architecture I would favour
For a modern platform with many interfaces, I would probably choose this combination:
```text
UI PLATFORM
┌──────────┐
│ App Shell │
└────┬─────┘
│
┌────────────────┼────────────────┐
│ │ │
routing UI context capability
layout identity registry
navigation theme discovery
locale
│
┌────────────┴────────────┐
│ UI Component Contract │
│ │
│ props / events │
│ commands / slots │
│ lifecycle / metadata │
└────────────┬────────────┘
│
┌─────────────────────┼────────────────────┐
│ │ │
Native Web Component Microfrontend
component boundary boundary
│ │ │
React React/Vue/etc independently
Vue internally deployed
Svelte
```
Underneath all of this sits a shared:
```text
Design Language
↓
Design Tokens
↓
Primitive Component Library
↓
Accessibility & interaction rules
```
The browser platform is becoming an increasingly credible basis for that middle interoperability layer. Web Components provide native reusable elements; Shadow DOM supplies encapsulation; slots provide host/component composition; and scoped custom-element registries address one of the historical problems of large compositions—global custom-element naming collisions. :chatgpt-content-reference{index="3"}
### One additional concept I think is worth making explicit
Given the way you tend to approach interface architecture, I would distinguish between a **UI component** and an **Interface Capability**.
An Interface Capability would be:
> A self-describing unit of human-system interaction that exposes a stable semantic contract independently of its rendering implementation.
For example:
```text
InterfaceCapability: DocumentViewer
provides:
view-document
browse-pages
download-document
accepts:
document-reference
page-reference
emits:
page-changed
document-opened
download-requested
requires:
document.read
presentation:
embeddable
fullscreen
implementations:
web-component
react-component
standalone-webapp
```
That abstraction scales much better than thinking exclusively in terms of React components or micro-frontends. A component becomes **one implementation of an interface capability**.
And that in turn gives you a rather elegant architecture:
```text
Interface Capability
↓
semantic contract
↓
one or more UI implementations
↓
composition/runtime adapter
↓
host application
```
This is close to what I would regard as the current best-practice direction for a genuinely complex multi-interface platform: **design system below, semantic capability contracts in the middle, composition shell above, and implementation technology deliberately hidden behind the boundary.**