diff --git a/packages/ui/src/components/Box/Box.module.css b/packages/ui/src/components/Box/Box.module.css
index 688358fda0..77200b297f 100644
--- a/packages/ui/src/components/Box/Box.module.css
+++ b/packages/ui/src/components/Box/Box.module.css
@@ -22,4 +22,20 @@
font-weight: var(--bui-font-weight-regular);
color: var(--bui-fg-primary);
}
+
+ .bui-Box[data-surface='0'] {
+ background-color: var(--bui-bg-surface-0);
+ }
+
+ .bui-Box[data-surface='1'] {
+ background-color: var(--bui-bg-surface-1);
+ }
+
+ .bui-Box[data-surface='2'] {
+ background-color: var(--bui-bg-surface-2);
+ }
+
+ .bui-Box[data-surface='3'] {
+ background-color: var(--bui-bg-surface-3);
+ }
}
diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx
index 766a6668c7..7033b75e79 100644
--- a/packages/ui/src/components/Box/Box.stories.tsx
+++ b/packages/ui/src/components/Box/Box.stories.tsx
@@ -18,6 +18,7 @@ import preview from '../../../../../.storybook/preview';
import { ReactNode } from 'react';
import { Box } from './Box';
import { Flex } from '../Flex';
+import { Button } from '../Button';
const meta = preview.meta({
title: 'Backstage UI/Box',
@@ -344,3 +345,50 @@ export const Display = meta.story({
),
});
+
+export const Surfaces = meta.story({
+ args: { px: '6', py: '4' },
+ render: args => (
+
+ Default
+
+ Surface 0
+
+
+ Surface 1
+
+
+ Surface 2
+
+
+ Surface 3
+
+
+ Responsive Surface
+
+
+ ),
+});
+
+export const SurfacesNested = meta.story({
+ args: { px: '6', py: '4' },
+ render: args => (
+
+
+ In this test, we are nesting boxes and buttons on different surfaces to
+ ensure that the correct surface is applied to each element. If a Button
+ is placed on a surface that doesn't have the surface prop set, it will
+ inherit the surface from the parent.
+
+
+
+
+
+
+
+
+
+
+
+ ),
+});
diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx
index 34c337824b..4864a27a0e 100644
--- a/packages/ui/src/components/Box/Box.tsx
+++ b/packages/ui/src/components/Box/Box.tsx
@@ -20,15 +20,14 @@ import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Box.module.css';
import { BoxDefinition } from './definition';
+import { SurfaceProvider } from '../../hooks/useSurface';
/** @public */
export const Box = forwardRef((props, ref) => {
- const { classNames, utilityClasses, style, cleanedProps } = useStyles(
- BoxDefinition,
- props,
- );
+ const { classNames, dataAttributes, utilityClasses, style, cleanedProps } =
+ useStyles(BoxDefinition, props);
- const { as = 'div', children, className, ...rest } = cleanedProps;
+ const { as = 'div', children, className, surface, ...rest } = cleanedProps;
return createElement(
as,
@@ -41,9 +40,14 @@ export const Box = forwardRef((props, ref) => {
className,
),
style,
+ ...dataAttributes,
...rest,
},
- children,
+ surface ? (
+ {children}
+ ) : (
+ children
+ ),
);
});
diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts
index f5b6fc6553..1bd1db97e6 100644
--- a/packages/ui/src/components/Box/definition.ts
+++ b/packages/ui/src/components/Box/definition.ts
@@ -48,4 +48,7 @@ export const BoxDefinition = {
'minHeight',
'maxHeight',
],
+ dataAttributes: {
+ surface: ['0', '1', '2', '3'] as const,
+ },
} as const satisfies ComponentDefinition;
diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts
index c0f4a71035..dd72c4b820 100644
--- a/packages/ui/src/components/Box/types.ts
+++ b/packages/ui/src/components/Box/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import type { SpaceProps, Responsive } from '../../types';
+import type { SpaceProps, Responsive, Surface } from '../../types';
/** @public */
export interface BoxProps extends SpaceProps {
@@ -32,4 +32,5 @@ export interface BoxProps extends SpaceProps {
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
+ surface?: Responsive;
}
diff --git a/packages/ui/src/css/tokens.css b/packages/ui/src/css/tokens.css
index a60107f9d2..83fb3cd0b0 100644
--- a/packages/ui/src/css/tokens.css
+++ b/packages/ui/src/css/tokens.css
@@ -80,24 +80,45 @@
--bui-gray-7: #757575;
--bui-gray-8: #595959;
- /* Background Colors */
+ /* Surfaces colors */
--bui-bg-surface-0: var(--bui-gray-1);
--bui-bg-surface-1: var(--bui-white);
--bui-bg-surface-2: var(--bui-gray-1);
--bui-bg-surface-3: var(--bui-gray-2);
+
+ /* Solid background colors */
--bui-bg-solid: #1f5493;
--bui-bg-solid-hover: #163a66;
--bui-bg-solid-pressed: #0f2b4e;
--bui-bg-solid-disabled: #ebebeb;
- --bui-bg-tint: transparent;
- --bui-bg-tint-hover: rgba(31, 84, 147, 0.4);
- --bui-bg-tint-pressed: rgba(31, 84, 147, 0.6);
- --bui-bg-tint-disabled: #ebebeb;
+
+ /* Neutral background colors */
+ --bui-bg-neutral-on-surface-0: oklch(0% 0 0 / 6%);
+ --bui-bg-neutral-on-surface-0-hover: oklch(0% 0 0 / 12%);
+ --bui-bg-neutral-on-surface-0-pressed: oklch(0% 0 0 / 16%);
+ --bui-bg-neutral-on-surface-0-disabled: oklch(0% 0 0 / 6%);
+
+ --bui-bg-neutral-on-surface-1: oklch(0% 0 0 / 6%);
+ --bui-bg-neutral-on-surface-1-hover: oklch(0% 0 0 / 12%);
+ --bui-bg-neutral-on-surface-1-pressed: oklch(0% 0 0 / 16%);
+ --bui-bg-neutral-on-surface-1-disabled: oklch(0% 0 0 / 6%);
+
+ --bui-bg-neutral-on-surface-2: oklch(0% 0 0 / 6%);
+ --bui-bg-neutral-on-surface-2-hover: oklch(0% 0 0 / 12%);
+ --bui-bg-neutral-on-surface-2-pressed: oklch(0% 0 0 / 16%);
+ --bui-bg-neutral-on-surface-2-disabled: oklch(0% 0 0 / 6%);
+
+ --bui-bg-neutral-on-surface-3: oklch(0% 0 0 / 6%);
+ --bui-bg-neutral-on-surface-3-hover: oklch(0% 0 0 / 12%);
+ --bui-bg-neutral-on-surface-3-pressed: oklch(0% 0 0 / 16%);
+ --bui-bg-neutral-on-surface-3-disabled: oklch(0% 0 0 / 6%);
+
+ /* Status background colors */
--bui-bg-danger: #feebe7;
--bui-bg-warning: #fff2b2;
--bui-bg-success: #e6f6eb;
- /* Foreground Colors */
+ /* Foreground colors */
--bui-fg-primary: var(--bui-black);
--bui-fg-secondary: var(--bui-gray-7);
--bui-fg-link: #1f5493;
@@ -111,7 +132,7 @@
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
- /* Border Colors */
+ /* Border colors */
--bui-border: rgba(0, 0, 0, 0.1);
--bui-border-hover: rgba(0, 0, 0, 0.2);
--bui-border-pressed: rgba(0, 0, 0, 0.4);
@@ -120,7 +141,7 @@
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
- /* Special Colors */
+ /* Special colors */
--bui-ring: #1f5493;
--bui-scrollbar: #a0a0a03b;
--bui-scrollbar-thumb: #a0a0a0;
@@ -140,24 +161,45 @@
--bui-gray-7: #9e9e9e;
--bui-gray-8: #b4b4b4;
- /* Background Colors */
+ /* Surfaces colors */
--bui-bg-surface-0: var(--bui-black);
--bui-bg-surface-1: var(--bui-gray-1);
--bui-bg-surface-2: var(--bui-gray-2);
--bui-bg-surface-3: var(--bui-gray-3);
+
+ /* Solid background colors */
--bui-bg-solid: #9cc9ff;
--bui-bg-solid-hover: #83b9fd;
--bui-bg-solid-pressed: #83b9fd;
--bui-bg-solid-disabled: #222222;
- --bui-bg-tint: transparent;
- --bui-bg-tint-hover: rgba(156, 201, 255, 0.12);
- --bui-bg-tint-pressed: rgba(156, 201, 255, 0.16);
- --bui-bg-tint-disabled: transparent;
+
+ /* Neutral background colors */
+ --bui-bg-neutral-on-surface-0: oklch(100% 0 0 / 10%);
+ --bui-bg-neutral-on-surface-0-hover: oklch(100% 0 0 / 14%);
+ --bui-bg-neutral-on-surface-0-pressed: oklch(100% 0 0 / 20%);
+ --bui-bg-neutral-on-surface-0-disabled: oklch(100% 0 0 / 10%);
+
+ --bui-bg-neutral-on-surface-1: oklch(100% 0 0 / 6%);
+ --bui-bg-neutral-on-surface-1-hover: oklch(100% 0 0 / 10%);
+ --bui-bg-neutral-on-surface-1-pressed: oklch(100% 0 0 / 16%);
+ --bui-bg-neutral-on-surface-1-disabled: oklch(100% 0 0 / 6%);
+
+ --bui-bg-neutral-on-surface-2: oklch(100% 0 0 / 8%);
+ --bui-bg-neutral-on-surface-2-hover: oklch(100% 0 0 / 12%);
+ --bui-bg-neutral-on-surface-2-pressed: oklch(100% 0 0 / 20%);
+ --bui-bg-neutral-on-surface-2-disabled: oklch(100% 0 0 / 8%);
+
+ --bui-bg-neutral-on-surface-3: oklch(100% 0 0 / 8%);
+ --bui-bg-neutral-on-surface-3-hover: oklch(100% 0 0 / 12%);
+ --bui-bg-neutral-on-surface-3-pressed: oklch(100% 0 0 / 20%);
+ --bui-bg-neutral-on-surface-3-disabled: oklch(100% 0 0 / 8%);
+
+ /* Status background colors */
--bui-bg-danger: #3b1219;
--bui-bg-warning: #302008;
--bui-bg-success: #132d21;
- /* Foreground Colors */
+ /* Foreground colors */
--bui-fg-primary: var(--bui-white);
--bui-fg-secondary: var(--bui-gray-7);
--bui-fg-link: #9cc9ff;
@@ -171,7 +213,7 @@
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
- /* Border Colors */
+ /* Border colors */
--bui-border: rgba(255, 255, 255, 0.12);
--bui-border-hover: rgba(255, 255, 255, 0.4);
--bui-border-pressed: rgba(255, 255, 255, 0.5);
@@ -180,7 +222,7 @@
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
- /* Special Colors */
+ /* Special colors */
--bui-ring: #1f5493;
--bui-scrollbar: #3636363a;
--bui-scrollbar-thumb: #575757;
diff --git a/packages/ui/src/hooks/useSurface.tsx b/packages/ui/src/hooks/useSurface.tsx
new file mode 100644
index 0000000000..50047bf721
--- /dev/null
+++ b/packages/ui/src/hooks/useSurface.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 { createContext, useContext, ReactNode } from 'react';
+import { Surface, Responsive } from '../types';
+
+/** @public */
+export interface SurfaceContextValue {
+ surface: Responsive | undefined;
+}
+
+/** @public */
+export interface SurfaceProviderProps {
+ surface: Responsive;
+ children: ReactNode;
+}
+
+const SurfaceContext = createContext({
+ surface: undefined,
+});
+
+/**
+ * Provider component that establishes the surface context for child components.
+ * This allows components to adapt their styling based on their background surface.
+ *
+ * @public
+ */
+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.
+ *
+ * @public
+ */
+export const useSurface = (): SurfaceContextValue => {
+ return useContext(SurfaceContext);
+};
diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts
index 7166194091..8b2ea94873 100644
--- a/packages/ui/src/types.ts
+++ b/packages/ui/src/types.ts
@@ -173,3 +173,9 @@ export interface ComponentDefinition {
dataAttributes?: DataAttributesMap;
utilityProps?: string[];
}
+
+/**
+ * Surface type
+ * @public
+ */
+export type Surface = '0' | '1' | '2' | '3';