Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/contributing/transpile-diff.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: Comparing Transpiled Output
category: Contributor Guides
category: Contributing
order: 8
---

# Comparing Transpiled Output
Expand Down
9 changes: 9 additions & 0 deletions docs/theming/legacy-theme-overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ relevantForAI: true

## Using theme overrides

```js
---
type: embed
---
<Alert variant="warning" margin="0 0 medium">
The examples on this page use the <strong>legacy theming system</strong> and are designed for <strong>v11.6</strong> components. If you are viewing the v11.7 version, <Link href={window.location.pathname.match(/v\d+_\d+/) ? window.location.pathname.replace(/v\d+_\d+/, 'v11_6') : `/v11_6${window.location.pathname}`}>switch to v11.6</Link> to see the examples working correctly.
</Alert>
```

This document gives an overview on how you can customize Instructure UI components by tweaking their theme variables.
While this gives you a level of flexibility on the look and feel of the components you should be aware of 2 things:

Expand Down
9 changes: 9 additions & 0 deletions docs/theming/new-theme-overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ relevantForAI: true

## New Theme Override Patterns

```js
---
type: embed
---
<Alert variant="warning" margin="0 0 medium">
The examples on this page use the <strong>new theming system</strong> and require <strong>v11.7+</strong> components. If you are viewing the v11.6 version, <Link href={window.location.pathname.match(/v\d+_\d+/) ? window.location.pathname.replace(/v\d+_\d+/, 'v11_7') : `/v11_7${window.location.pathname}`}>switch to v11.7</Link> to see the examples working correctly.
</Alert>
```

This guide covers all the override patterns available in the new theming system (v11.7+). The new system uses a layered token architecture: **primitives** (raw values) -> **semantics** (meaning) -> **components** (per-component tokens).

Overrides are applied via the `themeOverride` prop on `InstUISettingsProvider`, which is separate from the `theme` prop. The `theme` prop replaces the active theme entirely; `themeOverride` layers modifications on top.
Expand Down
7 changes: 5 additions & 2 deletions packages/__docs__/buildScripts/DataTypes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,14 @@ type ResolvedColors = {
semantic: Record<string, string>
}

// At runtime, build-docs.mts also attaches `resolvedComponents` (and on
// canvas / canvas-high-contrast: `key`, `description`) to the new-system
// entries. Not declared per-branch; surfaced as optional on `MainDocsData.themes`.
type ThemeResource =
| (BaseTheme & { resolvedComponents: Record<string, any> }) // legacy-canvas, legacy-canvas-high-contrast
| (NewBaseTheme & { resolvedColors: ResolvedColors }) // canvas, canvas-high-contrast
| (LightTheme & { resolvedColors: ResolvedColors })
| (DarkTheme & { resolvedColors: ResolvedColors })
| (LightTheme & { resolvedColors: ResolvedColors }) // light
| (DarkTheme & { resolvedColors: ResolvedColors }) // dark
| SharedTokens

type MainDocsData = {
Expand Down
35 changes: 31 additions & 4 deletions packages/__docs__/buildScripts/build-docs.mts
Original file line number Diff line number Diff line change
Expand Up @@ -420,17 +420,44 @@ function parseThemes() {
parsed['legacy-canvas-high-contrast'] = {
resource: { ...canvasHighContrast, resolvedComponents: resolveComponents(legacyCanvasHighContrast) }
}
// `key` is read by Document.tsx's `componentDidUpdate` to detect theme
// changes and refetch the Default Theme Variables. `legacyCanvas` /
// `legacyCanvasHighContrast` from `newThemeTokens` do not include a `key`
// field (unlike `light` / `dark`, which come through wrappers that set it).
// Without it, switching e.g. canvas (legacy) → canvas-high-contrast (legacy)
// on v11_7 leaves `themeVariables.key` `undefined` on both sides, so
// `undefined !== undefined` is false and the refetch never fires.
parsed['canvas'] = {
resource: { ...legacyCanvas, resolvedColors: resolveNewThemeColors(legacyCanvas), description: canvas.description }
resource: {
...legacyCanvas,
key: 'canvas',
resolvedColors: resolveNewThemeColors(legacyCanvas),
resolvedComponents: resolveComponents(legacyCanvas),
description: canvas.description
}
}
parsed['canvas-high-contrast'] = {
resource: { ...legacyCanvasHighContrast, resolvedColors: resolveNewThemeColors(legacyCanvasHighContrast), description: canvasHighContrast.description }
resource: {
...legacyCanvasHighContrast,
key: 'canvas-high-contrast',
resolvedColors: resolveNewThemeColors(legacyCanvasHighContrast),
resolvedComponents: resolveComponents(legacyCanvasHighContrast),
description: canvasHighContrast.description
}
}
parsed[light.key] = {
resource: { ...light, resolvedColors: resolveNewThemeColors(light.newTheme as typeof legacyCanvas) }
resource: {
...light,
resolvedColors: resolveNewThemeColors(light.newTheme as typeof legacyCanvas),
resolvedComponents: resolveComponents(light.newTheme as typeof legacyCanvas)
}
}
parsed[dark.key] = {
resource: { ...dark, resolvedColors: resolveNewThemeColors(dark.newTheme as typeof legacyCanvas) }
resource: {
...dark,
resolvedColors: resolveNewThemeColors(dark.newTheme as typeof legacyCanvas),
resolvedComponents: resolveComponents(dark.newTheme as typeof legacyCanvas)
}
}
const canvasSemantics = legacyCanvas.semantics(legacyCanvas.primitives)
parsed['shared-tokens'] = { resource: legacyCanvas.sharedTokens(canvasSemantics) }
Expand Down
13 changes: 12 additions & 1 deletion packages/__docs__/src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,13 @@ class App extends Component<AppProps, AppState> {
const allThemeKeys = Object.keys(this.state.docsData!.themes)
const showNewThemes = selectedMinorVersion !== 'v11_6'

// The `parsed.themes` map in build-docs.mts contains both:
// - `canvas` / `canvas-high-contrast` → new-system resources (primitives/semantics/sharedTokens/components`)
// - `legacy-canvas` / `legacy-canvas-high-contrast` → legacy wrappers (full theme object)
// v11_6 components' `generateComponentTheme(theme)` reads `theme.colors`,
// `theme.spacing`, etc., so v11_6 MUST be backed by the legacy wrappers.
// We pick `legacy-*` as the actual selected keys, and strip the prefix for
// display so the user still sees "canvas" / "canvas-high-contrast".
const themeKeys = showNewThemes
? allThemeKeys.filter(
(k) =>
Expand All @@ -610,7 +617,7 @@ class App extends Component<AppProps, AppState> {
k !== 'legacy-canvas-high-contrast'
)
: allThemeKeys.filter(
(k) => k === 'canvas' || k === 'canvas-high-contrast'
(k) => k === 'legacy-canvas' || k === 'legacy-canvas-high-contrast'
)

const displayThemeName = (themeKey: string) => {
Expand All @@ -620,6 +627,10 @@ class App extends Component<AppProps, AppState> {
) {
return `${themeKey} (legacy)`
}
// On v11_6 strip the `legacy-` prefix so the user sees the plain names.
if (!showNewThemes) {
return themeKey.replace(/^legacy-/, '')
}
return themeKey
}

Expand Down
65 changes: 43 additions & 22 deletions packages/__docs__/src/Document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { View } from '@instructure/ui-view'
import { Tabs } from '@instructure/ui-tabs'
import type { TabsProps } from '@instructure/ui-tabs'
import type { NewBaseTheme } from '@instructure/ui-themes'
import type { ComponentTheme as ComponentThemeData } from '@instructure/shared-types'
import { SourceCodeEditor } from '@instructure/ui-source-code-editor'
import { withStyleForDocs as withStyleNew } from '../withStyleForDocs'

Expand Down Expand Up @@ -80,47 +81,48 @@ class Document extends Component<DocumentProps, DocumentState> {

fetchGenerateComponentTheme = async () => {
const { doc, themeVariables } = this.props
let generateTheme
// Doc IDs use dot notation (e.g. "Menu.Item") but theme component keys
// use PascalCase without dots (e.g. "MenuItem").
// New-theme entries are in themeVariables.newTheme.components.
const selectedId = this.state.selectedDetailsTabId
type ComponentKey = keyof NewBaseTheme['components']
// When a child tab is selected, work from the child's doc/component;
// otherwise from the main doc's.
const childDoc =
selectedId !== doc.id
? doc?.children?.find((value) => value.id === selectedId)
: null
// in case of some components, we need to display the theme variables of other components based on themeId (like displaying the theme variables of Options in Drillsdown.Group)
const currentComponentInstance =
selectedId === doc.id
? doc?.componentInstance
: childDoc?.componentInstance
// Resolve the theme registry key. Default: doc id with dots stripped
// (e.g. "Menu.Item" → "MenuItem"). Two ways to override the default:
// - `themeId:` in YAML frontmatter (read from childDoc.themeId) — used
// when a doc page wants to show another component's tokens
// (e.g. Drilldown.Group → 'Options').
// - `static themeId` on the component class — used when a component
// borrows another's tokens via `useTokensFrom` at runtime
// (e.g. Button → 'BaseButton', ColorMixer.Slider → 'Slider').
const themeKey: ComponentKey = (childDoc?.themeId ||
currentComponentInstance?.themeId ||
selectedId?.replace(/\./g, '')) as ComponentKey
const isLegacyTheme = this.context?.componentVersion == 'v11_6'
// new theme
// New theme: tokens are pre-resolved into plain objects at build time
// (the build emits JSON, so generator functions can't be carried through).
if (!isLegacyTheme) {
// resolvedComponents contains pre-computed plain objects (built at build time)
const resolvedComponents = (themeVariables as Record<string, unknown>)
?.resolvedComponents as Record<string, unknown> | undefined
const newThemeEntry = resolvedComponents?.[themeKey as string]
const componentInstance =
selectedId === doc.id
? doc?.componentInstance
: childDoc?.componentInstance
if (
newThemeEntry &&
typeof componentInstance?.generateComponentTheme !== 'function'
typeof currentComponentInstance?.generateComponentTheme !== 'function'
) {
// new theme - use pre-computed theme object directly
this.setState({
componentTheme: newThemeEntry as DocumentState['componentTheme']
})
return
}
}
// old theme - use generateComponentTheme function
if (selectedId === doc.id) {
generateTheme = doc?.componentInstance?.generateComponentTheme
} else {
generateTheme = childDoc?.componentInstance?.generateComponentTheme
}
// Legacy theme: call the component's generateComponentTheme directly.
const generateTheme = currentComponentInstance?.generateComponentTheme
if (typeof generateTheme === 'function' && themeVariables) {
this.setState({ componentTheme: generateTheme(themeVariables) })
} else {
Expand Down Expand Up @@ -158,6 +160,19 @@ class Document extends Component<DocumentProps, DocumentState> {

const themeVariableKeys = componentTheme && Object.keys(componentTheme)

// The displayed token list comes from another component's registry when
// `themeId` (YAML frontmatter) or the component class's `static themeId`
// points at a different key than the doc's own id. In that case some of
// the listed tokens may not actually be used by this component.
const isLegacyTheme = this.context?.componentVersion === 'v11_6'
const dotStrippedId = doc.id?.replace(/\./g, '')
const borrowedThemeId =
doc.themeId || doc.componentInstance?.themeId
const borrowsTokens =
!isLegacyTheme &&
borrowedThemeId &&
borrowedThemeId !== dotStrippedId

return themeVariables &&
componentTheme &&
themeVariableKeys &&
Expand All @@ -166,6 +181,14 @@ class Document extends Component<DocumentProps, DocumentState> {
<Heading level="h2" as="h3" id={`${doc.id}Theme`} margin="0 0 small 0">
Default Theme Variables
</Heading>
{borrowsTokens ? (
<View as="div" margin="0 0 small 0">
Note: <code>{doc.id}</code> shares its theme tokens with{' '}
<code>{borrowedThemeId}</code>, so the table below lists every
token of <code>{borrowedThemeId}</code>. Some of these may not
actually be used by <code>{doc.id}</code>.
</View>
) : null}
{doc.themePath ? (
<View as="div" margin="0 0 x-small 0">
See which global theme variables are mapped to the component here:{' '}
Expand All @@ -181,9 +204,7 @@ class Document extends Component<DocumentProps, DocumentState> {
</code>
</View>
) : null}
<ComponentTheme
componentTheme={componentTheme as any /* TODO-theme-types check */}
/>
<ComponentTheme componentTheme={componentTheme as ComponentThemeData} />

<View margin="x-large 0 0" display="block">
<Heading
Expand Down
6 changes: 5 additions & 1 deletion packages/__docs__/src/Document/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,16 @@ type DocumentProps = DocumentOwnProps & WithStyleProps<null, DocumentStyle>

type DocumentStyle = ComponentStyle<'githubCornerOctoArm' | 'githubCorner'>

type ResolvedNewComponentTheme = ReturnType<
NewBaseTheme['components'][keyof NewBaseTheme['components']]
>

type DocumentState = {
selectedDetailsTabId: string | undefined
pageRef: HTMLDivElement | null
componentTheme:
| ThemeVariables[keyof ThemeVariables]
| NewBaseTheme['components'][keyof NewBaseTheme['components']]
| ResolvedNewComponentTheme
| undefined
}

Expand Down
2 changes: 2 additions & 0 deletions packages/ui-buttons/src/Button/v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ category: components
class Button extends Component<ButtonProps> {
static displayName = 'Button'
static readonly componentId = 'Button'
// Button v2 uses BaseButton's tokens; tell Document where to look for theme variables
static readonly themeId = 'BaseButton'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-buttons/src/CloseButton/v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ category: components
class CloseButton extends Component<CloseButtonProps> {
static displayName = 'CloseButton'
static readonly componentId = 'CloseButton'
// Uses BaseButton's tokens; tell Document where to look for theme variables
static readonly themeId = 'BaseButton'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-buttons/src/CondensedButton/v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ category: components
class CondensedButton extends Component<CondensedButtonProps> {
static displayName = 'CondensedButton'
static readonly componentId = 'CondensedButton'
// Uses BaseButton's tokens; tell Document where to look for theme variables
static readonly themeId = 'BaseButton'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-buttons/src/IconButton/v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ category: components
class IconButton extends Component<IconButtonProps> {
static displayName = 'IconButton'
static readonly componentId = 'IconButton'
// Uses BaseButton's tokens; tell Document where to look for theme variables
static readonly themeId = 'BaseButton'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ parent: Checkbox
class CheckboxFacade extends Component<CheckboxFacadeProps> {
static displayName = 'CheckboxFacade'
static readonly componentId = 'CheckboxFacade'
static readonly themeId = 'Checkbox'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ parent: Checkbox
class ToggleFacade extends Component<ToggleFacadeProps> {
static displayName = 'ToggleFacade'
static readonly componentId = 'ToggleFacade'
static readonly themeId = 'Toggle'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ColorPalette extends Component<ColorPaletteProps, ColorPaletteState> {
static displayName = 'ColorPalette'
static allowedProps = allowedProps
static readonly componentId = 'ColorMixer.Palette'
static readonly themeId = 'Palette'

constructor(props: ColorPaletteProps) {
super(props)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class RGBAInput extends Component<RGBAInputProps, RGBAInputState> {
static displayName = 'RGBAInput'
static allowedProps = allowedProps
static readonly componentId = 'ColorMixer.RGBAInput'
static readonly themeId = 'RgbaInput'

static defaultProps = {
withAlpha: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Slider extends Component<SliderProps> {
static displayName = 'Slider'
static allowedProps = allowedProps
static readonly componentId = 'ColorMixer.Slider'
static readonly themeId = 'Slider'

static defaultProps = {
isColorSlider: false
Expand Down
1 change: 1 addition & 0 deletions packages/ui-modal/src/Modal/v2/ModalBody/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ id: Modal.Body
class ModalBody extends Component<ModalBodyProps> {
static displayName = 'ModalBody'
static readonly componentId = 'Modal.Body'
static readonly themeId = 'ModalBody'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
1 change: 1 addition & 0 deletions packages/ui-modal/src/Modal/v2/ModalFooter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ id: Modal.Footer
class ModalFooter extends Component<ModalFooterProps> {
static displayName = 'ModalFooter'
static readonly componentId = 'Modal.Footer'
static readonly themeId = 'ModalFooter'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
1 change: 1 addition & 0 deletions packages/ui-modal/src/Modal/v2/ModalHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ id: Modal.Header
class ModalHeader extends Component<ModalHeaderProps> {
static displayName = 'ModalHeader'
static readonly componentId = 'Modal.Header'
static readonly themeId = 'ModalHeader'

static allowedProps = allowedProps
static defaultProps = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-number-input/src/NumberInput/v2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ const NumberInput = forwardRef<NumberInputHandle, NumberInputProps>(
)

NumberInput.displayName = 'NumberInput'
// Uses TextInput's tokens; tell Document where to look for theme variables
;(NumberInput as typeof NumberInput & { themeId: string }).themeId = 'TextInput'

export interface NumberInputHandle {
focus: () => void
Expand Down
1 change: 1 addition & 0 deletions packages/ui-pagination/src/Pagination/v1/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class Pagination extends Component<PaginationProps> {

static Page = PaginationButton
static Navigation = PaginationArrowButton
static PageInput = PaginationPageInput

private readonly _labelId: string

Expand Down
Loading
Loading