diff --git a/.changeset/ui-searchfield-textfield-bg-focus.md b/.changeset/ui-searchfield-textfield-bg-focus.md
new file mode 100644
index 0000000000..9f9cc02768
--- /dev/null
+++ b/.changeset/ui-searchfield-textfield-bg-focus.md
@@ -0,0 +1,7 @@
+---
+'@backstage/ui': patch
+---
+
+`SearchField` and `TextField` now automatically adapt their background color based on the parent bg context, stepping up one neutral level (e.g. neutral-1 → neutral-2) when placed on a neutral background. `TextField` also gains a focus ring using the `--bui-ring` token.
+
+**Affected components:** SearchField, TextField
diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt
index b596ef09e5..11df7fc1a0 100644
--- a/.github/vale/config/vocabularies/Backstage/accept.txt
+++ b/.github/vale/config/vocabularies/Backstage/accept.txt
@@ -30,6 +30,7 @@ backend's
backported
backporting
BEPs
+bg
bigint
Bigtable
Billett
@@ -425,6 +426,7 @@ scrollable
scrollbar
scrollbars
sdks
+SearchField
seb
semlas
semver
@@ -494,6 +496,7 @@ templater
Templater
templaters
Templaters
+TextField
TFRecord
theia
Themer
diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md
index b51c9722ed..f1d3505ccf 100644
--- a/packages/ui/report.api.md
+++ b/packages/ui/report.api.md
@@ -1968,6 +1968,7 @@ export const SearchFieldDefinition: {
readonly input: 'bui-SearchFieldInput';
readonly inputIcon: 'bui-SearchFieldInputIcon';
};
+ readonly bg: 'consumer';
readonly propDefs: {
readonly startCollapsed: {
readonly dataAttribute: true;
@@ -2534,6 +2535,7 @@ export const TextFieldDefinition: {
readonly inputIcon: 'bui-InputIcon';
readonly inputAction: 'bui-InputAction';
};
+ readonly bg: 'consumer';
readonly propDefs: {
readonly size: {
readonly dataAttribute: true;
diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.module.css b/packages/ui/src/components/PluginHeader/PluginHeader.module.css
index 9b6cc7d77d..f016ef7674 100644
--- a/packages/ui/src/components/PluginHeader/PluginHeader.module.css
+++ b/packages/ui/src/components/PluginHeader/PluginHeader.module.css
@@ -22,7 +22,6 @@
flex-direction: row;
align-items: center;
justify-content: space-between;
- background-color: var(--bui-bg-neutral-1);
padding-inline: var(--bui-space-5);
border-bottom: 1px solid var(--bui-border-1);
color: var(--bui-fg-primary);
@@ -67,6 +66,5 @@
.bui-PluginHeaderTabsWrapper {
padding-inline: var(--bui-space-3);
border-bottom: 1px solid var(--bui-border-1);
- background-color: var(--bui-bg-neutral-1);
}
}
diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.tsx
index 6a6dee3016..ac5f04a011 100644
--- a/packages/ui/src/components/PluginHeader/PluginHeader.tsx
+++ b/packages/ui/src/components/PluginHeader/PluginHeader.tsx
@@ -21,6 +21,7 @@ import { PluginHeaderDefinition } from './definition';
import { type NavigateOptions } from 'react-router-dom';
import { useRef } from 'react';
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
+import { Box } from '../Box';
import { Link } from 'react-aria-components';
import { RiShapesLine } from '@remixicon/react';
import { Text } from '../Text';
@@ -115,7 +116,7 @@ export const PluginHeader = (props: PluginHeaderProps) => {
{tabs && (
-
+
{tabs?.map(tab => (
@@ -130,7 +131,7 @@ export const PluginHeader = (props: PluginHeaderProps) => {
))}
-
+
)}
);
diff --git a/packages/ui/src/components/SearchField/SearchField.module.css b/packages/ui/src/components/SearchField/SearchField.module.css
index f4415aa0f8..b52c313a7d 100644
--- a/packages/ui/src/components/SearchField/SearchField.module.css
+++ b/packages/ui/src/components/SearchField/SearchField.module.css
@@ -39,6 +39,18 @@
}
}
+ &[data-on-bg='neutral-1'] .bui-SearchFieldInputWrapper {
+ background-color: var(--bui-bg-neutral-2);
+ }
+
+ &[data-on-bg='neutral-2'] .bui-SearchFieldInputWrapper {
+ background-color: var(--bui-bg-neutral-3);
+ }
+
+ &[data-on-bg='neutral-3'] .bui-SearchFieldInputWrapper {
+ background-color: var(--bui-bg-neutral-4);
+ }
+
&[data-startcollapsed='true'] {
transition: flex-basis 0.3s ease-in-out, width 0.3s ease-in-out,
max-width 0.3s ease-in-out;
@@ -93,9 +105,16 @@
display: flex;
align-items: center;
border-radius: var(--bui-radius-2);
- border: 1px solid var(--bui-border-2);
background-color: var(--bui-bg-neutral-1);
- transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
+ transition: box-shadow 0.2s ease-in-out;
+
+ &:has([data-focused]) {
+ box-shadow: inset 0 0 0 1px var(--bui-ring);
+ }
+
+ &:has([data-invalid]) {
+ box-shadow: inset 0 0 0 1px var(--bui-border-danger);
+ }
&[data-size='small'] {
height: 2rem;
@@ -105,10 +124,6 @@
height: 2.5rem;
}
- &[data-invalid] {
- border-color: var(--bui-fg-danger);
- }
-
&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx
index 7b75cfd24a..70784f3206 100644
--- a/packages/ui/src/components/SearchField/SearchField.stories.tsx
+++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx
@@ -19,6 +19,8 @@ import { useState } from 'react';
import { SearchField } from './SearchField';
import { Form } from 'react-aria-components';
import { Flex } from '../Flex';
+import { Box } from '../Box';
+import { Text } from '../Text';
import { FieldLabel } from '../FieldLabel';
import { ButtonIcon } from '../ButtonIcon';
import { RiCactusLine, RiEBike2Line } from '@remixicon/react';
@@ -341,3 +343,38 @@ export const StartCollapsedControlledWithValue = meta.story({
);
},
});
+
+export const AutoBg = meta.story({
+ render: () => (
+
+
+ SearchField automatically detects its parent bg context and increments
+ the neutral level by 1. No prop is needed — it's fully automatic.
+
+
+ Neutral 1 container
+
+
+
+
+
+
+ Neutral 2 container
+
+
+
+
+
+
+
+
+ Neutral 3 container
+
+
+
+
+
+
+
+ ),
+});
diff --git a/packages/ui/src/components/SearchField/definition.ts b/packages/ui/src/components/SearchField/definition.ts
index 76134ed0bb..a14da0a762 100644
--- a/packages/ui/src/components/SearchField/definition.ts
+++ b/packages/ui/src/components/SearchField/definition.ts
@@ -31,6 +31,7 @@ export const SearchFieldDefinition = defineComponent()({
input: 'bui-SearchFieldInput',
inputIcon: 'bui-SearchFieldInputIcon',
},
+ bg: 'consumer',
propDefs: {
startCollapsed: { dataAttribute: true, default: false },
size: { dataAttribute: true, default: 'small' },
diff --git a/packages/ui/src/components/TextField/TextField.module.css b/packages/ui/src/components/TextField/TextField.module.css
index a40c1dc4a7..bfef441f51 100644
--- a/packages/ui/src/components/TextField/TextField.module.css
+++ b/packages/ui/src/components/TextField/TextField.module.css
@@ -23,6 +23,18 @@
font-family: var(--bui-font-regular);
width: 100%;
flex-shrink: 0;
+
+ &[data-on-bg='neutral-1'] .bui-Input {
+ background-color: var(--bui-bg-neutral-2);
+ }
+
+ &[data-on-bg='neutral-2'] .bui-Input {
+ background-color: var(--bui-bg-neutral-3);
+ }
+
+ &[data-on-bg='neutral-3'] .bui-Input {
+ background-color: var(--bui-bg-neutral-4);
+ }
}
.bui-InputWrapper {
@@ -75,17 +87,22 @@
align-items: center;
padding: 0 var(--bui-space-3);
border-radius: var(--bui-radius-2);
- border: 1px solid var(--bui-border-2);
+ border: none;
background-color: var(--bui-bg-neutral-1);
font-size: var(--bui-font-size-3);
font-family: var(--bui-font-regular);
font-weight: var(--bui-font-weight-regular);
color: var(--bui-fg-primary);
- transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
+ transition: box-shadow 0.2s ease-in-out;
width: 100%;
height: 100%;
cursor: inherit;
+ &[data-focused] {
+ outline: none;
+ box-shadow: inset 0 0 0 1px var(--bui-ring);
+ }
+
&::-webkit-search-cancel-button,
&::-webkit-search-decoration {
-webkit-appearance: none;
@@ -96,7 +113,7 @@
}
&[data-invalid] {
- border-color: var(--bui-fg-danger);
+ box-shadow: inset 0 0 0 1px var(--bui-border-danger);
}
&[data-disabled] {
diff --git a/packages/ui/src/components/TextField/TextField.stories.tsx b/packages/ui/src/components/TextField/TextField.stories.tsx
index 03c5f35b11..43099046bc 100644
--- a/packages/ui/src/components/TextField/TextField.stories.tsx
+++ b/packages/ui/src/components/TextField/TextField.stories.tsx
@@ -17,6 +17,8 @@ import preview from '../../../../../.storybook/preview';
import { TextField } from './TextField';
import { Form } from 'react-aria-components';
import { Flex } from '../Flex';
+import { Box } from '../Box';
+import { Text } from '../Text';
import { FieldLabel } from '../FieldLabel';
import { RiEyeLine, RiSparklingLine } from '@remixicon/react';
@@ -145,3 +147,46 @@ export const CustomField = meta.story({
>
),
});
+
+export const AutoBg = meta.story({
+ render: () => (
+
+
+ TextField automatically detects its parent bg context and increments the
+ neutral level by 1. No prop is needed — it's fully automatic.
+
+
+ Neutral 1 container
+
+
+
+
+
+
+ Neutral 2 container
+
+
+
+
+
+
+
+
+ Neutral 3 container
+
+
+
+
+
+
+
+ ),
+});
diff --git a/packages/ui/src/components/TextField/definition.ts b/packages/ui/src/components/TextField/definition.ts
index ddfc949e5c..3d9dcc88fc 100644
--- a/packages/ui/src/components/TextField/definition.ts
+++ b/packages/ui/src/components/TextField/definition.ts
@@ -31,6 +31,7 @@ export const TextFieldDefinition = defineComponent()({
inputIcon: 'bui-InputIcon',
inputAction: 'bui-InputAction',
},
+ bg: 'consumer',
propDefs: {
size: { dataAttribute: true, default: 'small' },
className: {},