Skip to content

RFC: expose host brand color ramps through app context and theme changes #84

Description

@Ashlesha-MSFT

Summary
Extend TeamsJS so hosts can provide a complete brand color ramp to hosted applications.

The ramp is available through:

app.getContext() for initial and recoverable appearance state.
app.registerOnThemeChangeHandler() for updates during the app lifecycle, including ramp-only changes where the theme name remains unchanged.
The API uses a TeamsJS-owned structural type and does not introduce a dependency on Fluent UI.

Motivation
TeamsJS currently exposes only the host's coarse theme name, such as default, dark, or contrast.

That value is not enough for hosts that use a custom brand palette. Hosted applications need the complete color ramp to construct a theme that matches the host rather than relying on a locally generated or hard-coded brand ramp.

The ramp must be available both:

When the application initializes, reloads, or resumes.
When the host changes its brand ramp while the application is already running.
A ramp update does not necessarily change the coarse theme name, so existing theme-name comparisons alone cannot detect every appearance update.

Proposed public API
Add a TeamsJS-owned structural ramp type:

export type BrandColorRamp = Record<
10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 |
90 | 100 | 110 | 120 | 130 | 140 | 150 | 160,
string

;
Add the optional ramp to structured app context:

export interface AppInfo {
locale: string;
theme: string;
brandVariants?: BrandColorRamp;
// Existing properties...
}
Add the corresponding field to the deprecated legacy context so the host response can be transformed into the structured context:

export interface Context {
theme?: string;
brandVariants?: BrandColorRamp;
// Existing properties...
}
Extend the theme-change callback with an optional second argument:

export type themeHandler = (
theme: string,
brandVariants?: BrandColorRamp
) => void;
The deprecated callback type receives the same additive change:

export type registerOnThemeChangeHandlerFunctionType = (
theme: string,
brandVariants?: BrandColorRamp
) => void;
Initial context behavior
The host returns the ramp as a top-level field in the legacy context response:

{
theme: "default",
brandVariants: {
10: "#020305",
// ...
160: "#f5f7ff"
}
}
transformLegacyContextToAppContext() maps it into the structured app context:

{
app: {
theme: "default",
brandVariants: {
10: "#020305",
// ...
160: "#f5f7ff"
}
}
}
If the host does not provide a ramp, context.app.brandVariants remains undefined.

Runtime theme-change behavior
The existing themeChange event name and registration protocol remain unchanged.

A host without a custom ramp continues sending one argument:

["default"]
A host with a custom ramp sends an optional second argument:

["default", brandColorRamp]
TeamsJS forwards both values to the registered callback:

app.registerOnThemeChangeHandler((theme, brandVariants) => {
// Apply the updated appearance.
});
This event may be raised when:

The coarse theme changes.
The brand ramp changes while the theme name remains unchanged.
A custom ramp is added or removed.
When the ramp is removed, the callback receives undefined as its second argument.

Nested-frame behavior
When TeamsJS relays themeChange to a nested child frame, it preserves the same argument shape:

[theme]
or:

[theme, brandVariants]
This keeps framed, frameless, and nested application behavior consistent.

Host responsibilities
A supporting host:

Includes brandVariants in the context returned by getContext.
Raises themeChange when either the theme name or brand ramp changes.
Supplies all 16 ramp shades when a custom ramp is present.
Omits the ramp when no custom brand ramp is available.
The corresponding host transport is introduced by Hub SDK PR #5737682.

Why context and themeChange are both needed
App context provides a snapshot of the current appearance during initialization, reload, and resume. It does not require the application to have observed an earlier event.

themeChange provides a live notification when appearance changes during the current lifecycle.

Making the ramp event-only would allow applications that register late or restart to miss the current value. Making it context-only would require applications to subscribe to broad context updates to detect appearance changes.

Why extend themeChange instead of adding a new event
The ramp is part of the host's appearance and changes for the same reason as the coarse theme.
Existing applications already use registerOnThemeChangeHandler for appearance updates.
An optional second argument is backward compatible with existing JavaScript callbacks.
A separate event would require another registration and could produce duplicate or incorrectly ordered appearance updates.
Why use a TeamsJS-owned type
TeamsJS should not expose a Fluent UI type as part of its public API.

The structural BrandColorRamp contract:

Avoids adding a Fluent UI dependency.
Keeps the transport independent of a rendering library.
Remains structurally compatible with Fluent UI v9 BrandVariants.
Allows other UI systems to consume the same color data.
Backward compatibility
The change is additive:

Existing hosts can omit brandVariants.
Existing one-argument handlers continue receiving theme as their first argument.
Existing JavaScript handlers ignore the additional argument.
New applications receive undefined when running in an older or unsupported host.
The event name and registration message remain unchanged.
Feature flag assessment
No new TeamsJS feature flag is proposed.

The context field and callback argument are optional. Hosts opt into the behavior by supplying a ramp, while omission preserves existing behavior.

Alternatives considered
Use only contextChange
Rejected because appearance consumers would need to subscribe to every context update and filter unrelated changes.

Put the ramp only on themeChange
Rejected because events are transient and do not provide the initial or recoverable appearance snapshot.

Add a separate brandVariantsChange event
Rejected because it would create two appearance subscriptions and introduce ordering and deduplication concerns when the theme and ramp change together.

Put the ramp in host.features
Rejected because host features describe capabilities, not current appearance state.

Export Fluent UI's BrandVariants type
Rejected because it would couple the TeamsJS public contract to a specific UI library.

Implementation
TeamsJS implementation: OfficeDev/microsoft-teams-library-js#3170
Hub SDK transport: PR #5737682

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    AI-generated responseAn AI-generated response has been posted to this issue.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions