,
const C extends ComponentConfig,
>(
- config: C & SurfacePropsConstraint
,
+ config: C & BgPropsConstraint
,
): C => config;
}
diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts
index 3cef56148e..91ebdb8ef5 100644
--- a/packages/ui/src/hooks/useDefinition/types.ts
+++ b/packages/ui/src/hooks/useDefinition/types.ts
@@ -36,25 +36,25 @@ export interface ComponentConfig<
propDefs: { [K in keyof P]: PropDefConfig
};
// readonly for compatibility with const inference from factory
utilityProps?: readonly UtilityPropKey[];
- surface?: 'container' | 'leaf';
+ /**
+ * How this component participates in the bg system.
+ *
+ * - `'provider'` — calls `useBgProvider`, sets `data-bg`, wraps children in `BgProvider`
+ * - `'consumer'` — calls `useBgConsumer`, sets `data-on-bg`
+ */
+ bg?: 'provider' | 'consumer';
}
/**
- * Type constraint that validates surface props are present in the props type.
- * - If surface is 'leaf', P must include 'onSurface'
- * - If surface is 'container', P must include 'surface'
+ * Type constraint that validates bg props are present in the props type.
+ * - Provider components must include 'bg' in their props
+ * - Consumer components don't need a bg prop
*/
-export type SurfacePropsConstraint
= Surface extends 'leaf'
- ? 'onSurface' extends keyof P
+export type BgPropsConstraint
= Bg extends 'provider'
+ ? 'bg' extends keyof P
? {}
: {
- __error: 'Leaf components must include onSurface in props type. Extend LeafProps.';
- }
- : Surface extends 'container'
- ? 'surface' extends keyof P
- ? {}
- : {
- __error: 'Container components must include surface in props type. Extend ContainerProps.';
+ __error: 'Bg provider components must include bg in props type.';
}
: {};
@@ -81,12 +81,10 @@ type ResolvedOwnProps<
[K in keyof PropDefs & keyof P]: ResolvePropType
;
};
-type ChildrenProps =
- Surface extends 'container'
- ? { surfaceChildren: ReactNode; children?: never }
- : Surface extends 'leaf'
- ? { children: ReactNode; surfaceChildren?: never }
- : { children: ReactNode };
+type ChildrenProps =
+ Bg extends 'provider'
+ ? { childrenWithBgProvider: ReactNode; children?: never }
+ : { children: ReactNode; childrenWithBgProvider?: never };
type DataAttributeKeys = {
[K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true }
@@ -98,7 +96,7 @@ type DataAttributes = {
[K in DataAttributeKeys as `data-${Lowercase<
string & K
>}`]?: string;
-} & { 'data-on-surface'?: string };
+} & { 'data-bg'?: string; 'data-on-bg'?: string };
export type UtilityKeys> =
D['utilityProps'] extends ReadonlyArray ? K : never;
@@ -127,7 +125,7 @@ export interface UseDefinitionResult<
ownProps: {
classes: Record;
} & ResolvedOwnProps &
- ChildrenProps;
+ ChildrenProps;
// Rest props excludes both propDefs keys AND utility prop keys
restProps: keyof Omit> extends never
diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx
index 0de2eb8358..d1fe9af96c 100644
--- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx
+++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx
@@ -17,7 +17,7 @@
import { ReactNode } from 'react';
import clsx from 'clsx';
import { useBreakpoint } from '../useBreakpoint';
-import { useSurface, SurfaceProvider, UseSurfaceOptions } from '../useSurface';
+import { useBgProvider, useBgConsumer, BgProvider } from '../useBg';
import { resolveResponsiveValue, processUtilityProps } from './helpers';
import type {
ComponentConfig,
@@ -36,14 +36,13 @@ export function useDefinition<
): UseDefinitionResult {
const { breakpoint } = useBreakpoint();
- const surfaceOptions: UseSurfaceOptions | undefined =
- definition.surface === 'container'
- ? { surface: props.surface }
- : definition.surface === 'leaf'
- ? { onSurface: props.onSurface }
- : undefined;
+ // Provider: resolve bg and provide context for children
+ const providerBg = useBgProvider(
+ definition.bg === 'provider' ? props.bg : undefined,
+ );
- const { surface: resolvedSurface } = useSurface(surfaceOptions);
+ // Consumer: read parent context bg
+ const consumerBg = useBgConsumer();
const ownPropKeys = new Set(Object.keys(definition.propDefs));
const utilityPropKeys = new Set(definition.utilityProps ?? []);
@@ -70,6 +69,9 @@ export function useDefinition<
if (finalValue !== undefined) {
ownPropsResolved[key] = finalValue;
+ // Skip data-bg for bg prop when the provider path handles it
+ if (key === 'bg' && definition.bg === 'provider') continue;
+
if ((config as any).dataAttribute) {
// eslint-disable-next-line no-restricted-syntax
dataAttributes[`data-${key.toLowerCase()}`] = String(finalValue);
@@ -77,16 +79,14 @@ export function useDefinition<
}
}
- // Add data-on-surface for leaf components
- if (definition.surface === 'leaf' && resolvedSurface !== undefined) {
- // Handle responsive surface values - for data attributes, use the resolved string
- const surfaceValue =
- typeof resolvedSurface === 'object'
- ? resolveResponsiveValue(resolvedSurface as any, breakpoint)
- : resolvedSurface;
- if (surfaceValue !== undefined) {
- dataAttributes['data-on-surface'] = String(surfaceValue);
- }
+ // Provider: set data-bg from the resolved provider bg
+ if (definition.bg === 'provider' && providerBg.bg !== undefined) {
+ dataAttributes['data-bg'] = String(providerBg.bg);
+ }
+
+ // Consumer: set data-on-bg from the parent context
+ if (definition.bg === 'consumer' && consumerBg.bg !== undefined) {
+ dataAttributes['data-on-bg'] = String(consumerBg.bg);
}
const { utilityClasses, utilityStyle } = processUtilityProps>(
@@ -109,13 +109,11 @@ export function useDefinition<
}
let children: ReactNode | undefined;
- let surfaceChildren: ReactNode | undefined;
+ let childrenWithBgProvider: ReactNode | undefined;
- if (definition.surface === 'container') {
- surfaceChildren = resolvedSurface ? (
-
- {props.children}
-
+ if (definition.bg === 'provider') {
+ childrenWithBgProvider = providerBg.bg ? (
+ {props.children}
) : (
props.children
);
@@ -127,8 +125,8 @@ export function useDefinition<
ownProps: {
classes,
...ownPropsResolved,
- ...(definition.surface === 'container'
- ? { surfaceChildren }
+ ...(definition.bg === 'provider'
+ ? { childrenWithBgProvider }
: { children }),
},
restProps,
diff --git a/packages/ui/src/hooks/useSurface.tsx b/packages/ui/src/hooks/useSurface.tsx
deleted file mode 100644
index a088fb3b63..0000000000
--- a/packages/ui/src/hooks/useSurface.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright 2025 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { useContext, ReactNode } from 'react';
-import {
- createVersionedContext,
- createVersionedValueMap,
-} from '@backstage/version-bridge';
-import { Surface, Responsive } from '../types';
-
-/** @public */
-export interface SurfaceContextValue {
- surface: Responsive | undefined;
-}
-
-/** @public */
-export interface SurfaceProviderProps {
- surface: Responsive;
- children: ReactNode;
-}
-
-/** @public */
-export interface UseSurfaceOptions {
- /**
- * The surface value this component CREATES for its children (container behavior).
- * When 'auto', increments from parent surface.
- *
- * Use this for components like Box, Flex, Grid that establish surface context.
- */
- surface?: Responsive;
- /**
- * The surface value this component is ON for styling (leaf behavior).
- * When 'auto', inherits from current surface.
- *
- * Use this for leaf components like Button that consume surface for styling.
- */
- onSurface?: Responsive;
-}
-
-const SurfaceContext = createVersionedContext<{
- 1: SurfaceContextValue;
-}>('surface-context');
-
-/**
- * Increments a surface level by one, capping at '3'.
- * Intent surfaces (danger, warning, success) remain unchanged.
- *
- * @internal
- */
-function incrementSurface(surface: Surface | undefined): Surface {
- if (!surface) return '0'; // no context = root level
- if (surface === '0') return '1';
- if (surface === '1') return '2';
- if (surface === '2' || surface === '3') return '3'; // cap at max
- // Intent surfaces remain unchanged
- if (surface === 'danger') return 'danger';
- if (surface === 'warning') return 'warning';
- if (surface === 'success') return 'success';
- // 'auto' should not appear here, but handle it defensively
- if (surface === 'auto') return '1';
- return surface;
-}
-
-/**
- * Resolves a surface value for containers (SurfaceProvider).
- * When 'auto' is used, increments from the parent surface.
- * For responsive surfaces (objects), returns them as-is without resolution.
- *
- * @param contextSurface - The surface from context
- * @param requestedSurface - The requested surface value (may be 'auto')
- * @returns The resolved surface value
- * @internal
- */
-export function resolveSurfaceForProvider(
- contextSurface: Responsive | undefined,
- requestedSurface: Responsive | undefined,
-): Responsive | undefined {
- if (!requestedSurface) {
- return contextSurface;
- }
-
- // If requestedSurface is a responsive object (breakpoint-based), return as-is
- if (typeof requestedSurface === 'object') {
- return requestedSurface;
- }
-
- // If contextSurface is a responsive object, we can't auto-increment from it
- // Return the requested surface as-is or default to '0' for auto
- if (typeof contextSurface === 'object') {
- if (requestedSurface === 'auto') {
- return '0'; // fallback to root when context is responsive
- }
- return requestedSurface;
- }
-
- // For containers, 'auto' means increment to create a new elevated context
- if (requestedSurface === 'auto') {
- return incrementSurface(contextSurface);
- }
-
- return requestedSurface;
-}
-
-/**
- * Resolves a surface value for leaf components (useSurface hook).
- * When 'auto' is used, inherits the current surface (doesn't increment).
- * For responsive surfaces (objects), returns them as-is without resolution.
- *
- * @param contextSurface - The surface from context
- * @param requestedSurface - The requested surface value (may be 'auto')
- * @returns The resolved surface value
- * @internal
- */
-function resolveSurfaceForConsumer(
- contextSurface: Responsive | undefined,
- requestedSurface: Responsive | undefined,
-): Responsive | undefined {
- if (!requestedSurface) {
- return contextSurface;
- }
-
- // If requestedSurface is a responsive object (breakpoint-based), return as-is
- if (typeof requestedSurface === 'object') {
- return requestedSurface;
- }
-
- // For leaf components, 'auto' means inherit the current surface
- if (requestedSurface === 'auto') {
- // If context is responsive, fallback to '0'
- if (typeof contextSurface === 'object') {
- return '0';
- }
- return contextSurface;
- }
-
- return requestedSurface;
-}
-
-/**
- * Provider component that establishes the surface context for child components.
- * This allows components to adapt their styling based on their background surface.
- *
- * Note: The surface value should already be resolved before passing to this provider.
- * Container components should use useSurface with the surface parameter.
- *
- * @internal
- */
-export const SurfaceProvider = ({
- surface,
- children,
-}: SurfaceProviderProps) => {
- return (
-
- {children}
-
- );
-};
-
-/**
- * Hook to access the current surface context.
- * Returns the current surface level, or undefined if no provider is present.
- *
- * The parameter name determines the behavior:
- * - `surface`: Container behavior - 'auto' increments from parent
- * - `onSurface`: Leaf behavior - 'auto' inherits from parent
- *
- * @param options - Optional configuration for surface resolution
- * @internal
- */
-export const useSurface = (
- options?: UseSurfaceOptions,
-): SurfaceContextValue => {
- const value = useContext(SurfaceContext)?.atVersion(1);
- const context = value ?? { surface: undefined };
-
- // Infer behavior from which parameter is provided
- // 'surface' = provider behavior (increment)
- // 'onSurface' = consumer behavior (inherit)
- const isProvider = options?.surface !== undefined;
- const requestedSurface = options?.surface ?? options?.onSurface;
-
- const resolvedSurface = isProvider
- ? resolveSurfaceForProvider(context.surface, requestedSurface)
- : resolveSurfaceForConsumer(context.surface, requestedSurface);
-
- return {
- surface: resolvedSurface,
- };
-};
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 4ce616d5f1..c9f2a3763c 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -64,3 +64,5 @@ export * from './types';
// Hooks
export { useBreakpoint } from './hooks/useBreakpoint';
+export { useBgProvider, useBgConsumer, BgProvider } from './hooks/useBg';
+export type { BgContextValue, BgProviderProps } from './hooks/useBg';
diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts
index 933e1d8aa9..d3dfd996aa 100644
--- a/packages/ui/src/types.ts
+++ b/packages/ui/src/types.ts
@@ -182,29 +182,30 @@ export interface ComponentDefinition {
}
/**
- * Surface type
+ * Background type for the neutral bg system.
*
- * Supports absolute levels ('0'-'3'), intent surfaces ('danger', 'warning', 'success'),
- * and 'auto' which increments from the parent surface context.
+ * Supports neutral levels ('neutral-1' through 'neutral-3') and
+ * intent backgrounds ('danger', 'warning', 'success').
+ *
+ * The 'neutral-4' level is not exposed as a prop value -- it is reserved
+ * for leaf component CSS (e.g. Button on a 'neutral-3' surface).
*
* @public
*/
-export type Surface =
- | '0'
- | '1'
- | '2'
- | '3'
+export type ContainerBg =
+ | 'neutral-1'
+ | 'neutral-2'
+ | 'neutral-3'
| 'danger'
| 'warning'
- | 'success'
- | 'auto';
+ | 'success';
-/** @public */
-export interface LeafSurfaceProps {
- onSurface?: Responsive;
-}
-
-/** @public */
-export interface ContainerSurfaceProps {
- surface?: Responsive;
-}
+/**
+ * Background values accepted by provider components.
+ *
+ * Includes all `ContainerBg` values plus `'neutral-auto'` which
+ * automatically increments the neutral level from the parent context.
+ *
+ * @public
+ */
+export type ProviderBg = ContainerBg | 'neutral-auto';