Improve SearchField + TextField look

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-03-02 13:44:35 +00:00
parent 3438d4f800
commit 934ac03ce6
10 changed files with 129 additions and 10 deletions
@@ -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
@@ -29,7 +29,6 @@
left: 0px;
right: 0px;
height: 16px;
background-color: var(--bui-bg-app);
z-index: 0;
}
}
@@ -41,7 +40,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);
@@ -90,6 +88,5 @@
.bui-PluginHeaderTabsWrapper {
padding-inline: var(--bui-space-3);
border-bottom: 1px solid var(--bui-border-1);
background-color: var(--bui-bg-neutral-1);
}
}
@@ -22,6 +22,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';
declare module 'react-aria-components' {
interface RouterConfig {
@@ -92,7 +93,7 @@ export const PluginHeader = (props: PluginHeaderProps) => {
hasTabs={hasTabs}
/>
{tabs && (
<div className={classes.tabsWrapper}>
<Box bg="neutral" className={classes.tabsWrapper}>
<Tabs onSelectionChange={onTabSelectionChange}>
<TabList>
{tabs?.map(tab => (
@@ -107,7 +108,7 @@ export const PluginHeader = (props: PluginHeaderProps) => {
))}
</TabList>
</Tabs>
</div>
</Box>
)}
</header>
);
@@ -21,6 +21,7 @@ import { useRef } from 'react';
import { RiShapesLine } from '@remixicon/react';
import type { PluginHeaderToolbarProps } from './types';
import { Text } from '../Text';
import { Box } from '../Box';
/**
* A component that renders the toolbar section of a plugin header.
@@ -44,7 +45,7 @@ export const PluginHeaderToolbar = (props: PluginHeaderToolbarProps) => {
);
return (
<div className={classes.root} data-has-tabs={hasTabs}>
<Box bg="neutral" className={classes.root} data-has-tabs={hasTabs}>
<div className={classes.wrapper} ref={toolbarWrapperRef}>
<div className={classes.content} ref={toolbarContentRef}>
<Text as="h1" variant="body-medium">
@@ -61,6 +62,6 @@ export const PluginHeaderToolbar = (props: PluginHeaderToolbarProps) => {
{customActions}
</div>
</div>
</div>
</Box>
);
};
@@ -93,10 +93,21 @@
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;
.bui-SearchField[data-on-bg='neutral-1'] & {
background-color: var(--bui-bg-neutral-2);
}
.bui-SearchField[data-on-bg='neutral-2'] & {
background-color: var(--bui-bg-neutral-3);
}
.bui-SearchField[data-on-bg='neutral-3'] & {
background-color: var(--bui-bg-neutral-4);
}
&[data-size='small'] {
height: 2rem;
}
@@ -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: () => (
<Flex direction="column" gap="4">
<div style={{ maxWidth: '600px' }}>
SearchField automatically detects its parent bg context and increments
the neutral level by 1. No prop is needed it's fully automatic.
</div>
<Box bg="neutral" p="4">
<Text>Neutral 1 container</Text>
<Flex mt="2" style={{ maxWidth: '300px' }}>
<SearchField aria-label="Search" size="small" />
</Flex>
</Box>
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 2 container</Text>
<Flex mt="2" style={{ maxWidth: '300px' }}>
<SearchField aria-label="Search" size="small" />
</Flex>
</Box>
</Box>
<Box bg="neutral">
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 3 container</Text>
<Flex mt="2" style={{ maxWidth: '300px' }}>
<SearchField aria-label="Search" size="small" />
</Flex>
</Box>
</Box>
</Box>
</Flex>
),
});
@@ -31,6 +31,7 @@ export const SearchFieldDefinition = defineComponent<SearchFieldOwnProps>()({
input: 'bui-SearchFieldInput',
inputIcon: 'bui-SearchFieldInputIcon',
},
bg: 'consumer',
propDefs: {
startCollapsed: { dataAttribute: true, default: false },
size: { dataAttribute: true, default: 'small' },
@@ -75,17 +75,35 @@
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);
.bui-TextField[data-on-bg='neutral-1'] & {
background-color: var(--bui-bg-neutral-2);
}
.bui-TextField[data-on-bg='neutral-2'] & {
background-color: var(--bui-bg-neutral-3);
}
.bui-TextField[data-on-bg='neutral-3'] & {
background-color: var(--bui-bg-neutral-4);
}
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;
@@ -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: () => (
<Flex direction="column" gap="4">
<div style={{ maxWidth: '600px' }}>
TextField automatically detects its parent bg context and increments the
neutral level by 1. No prop is needed it's fully automatic.
</div>
<Box bg="neutral" p="4">
<Text>Neutral 1 container</Text>
<Flex mt="2" style={{ maxWidth: '300px' }}>
<TextField aria-label="Text" placeholder="Enter text" size="small" />
</Flex>
</Box>
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 2 container</Text>
<Flex mt="2" style={{ maxWidth: '300px' }}>
<TextField
aria-label="Text"
placeholder="Enter text"
size="small"
/>
</Flex>
</Box>
</Box>
<Box bg="neutral">
<Box bg="neutral">
<Box bg="neutral" p="4">
<Text>Neutral 3 container</Text>
<Flex mt="2" style={{ maxWidth: '300px' }}>
<TextField
aria-label="Text"
placeholder="Enter text"
size="small"
/>
</Flex>
</Box>
</Box>
</Box>
</Flex>
),
});
@@ -31,6 +31,7 @@ export const TextFieldDefinition = defineComponent<TextFieldOwnProps>()({
inputIcon: 'bui-InputIcon',
inputAction: 'bui-InputAction',
},
bg: 'consumer',
propDefs: {
size: { dataAttribute: true, default: 'small' },
className: {},