Skip to content

refactor: searchbar component - #5020

Open
MrMuzyk wants to merge 9 commits into
callstack:mainfrom
MrMuzyk:refactor/searchbar
Open

refactor: searchbar component#5020
MrMuzyk wants to merge 9 commits into
callstack:mainfrom
MrMuzyk:refactor/searchbar

Conversation

@MrMuzyk

@MrMuzyk MrMuzyk commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Motivation

Modernizes the Searchbar to the latest MD3 (Expressive) search spec by adopting the shared theme tokens (shape, motion, colors), extracting component-specific tokens, and adding the missing results container - following the token/structure conventions of the already-modernized TextInput/Switch/Checkbox/FAB. Also fixes the long-standing swapped input/placeholder colors and adds the missing focus transition.

Changes

Colors (tokens.ts / utils.ts)

  • Fixed swapped roles: input text onSurfaceVariant → onSurface; placeholder onSurface → onSurfaceVariant (MD3 order).
  • Container background resolved from the container token (surfaceContainerHigh) via getSearchbarColors, instead of a hardcoded color.

Shape

  • contained → corner.extraLarge (28dp), divided → none (0), resolved through the shared resolveCornerRadius util (src/theme/utils/shape.ts).

Motion - focus transition (new)

  • The contained bar grows a little wider when focused: its side margins shrink from 24dp to 12dp, then expand back on blur. It's a spring animation (Reanimated) and respects the OS "reduce motion" setting.
  • Note: contained mode (now the default) has side margins, where the old bar mode was full-width. Set a horizontal margin in style to override it — that also turns the focus animation off.

Structure

  • Moved Searchbar.tsx into its own src/components/Searchbar/ directory with tokens.ts (sizes/shape/colors), utils.ts (getSearchbarColors, getSearchbarInputFont), and
    index.tsx (attaches Results as a static sub-component) - mirroring Switch/Checkbox/FAB.

New sub-component — Searchbar.Results (SearchbarResults.tsx)

  • A Surface container for the results/suggestions list (MD3 search anatomy element 6). It provides only the surface; result grouping is left to the consumer. SearchbarResultsProps is exported from the package root.

Breaking change

Mode values are renamed to MD3 terminology

   // Before (v5)                                                                                                                                                          
   <Searchbar mode="bar" ... />   // default                                                                                                                               
   <Searchbar mode="view" ... />                                                                                                                                           
                                                                                                                                                                           
   // After (v6)                                                                                                                                                           
   <Searchbar mode="contained" ... />  // default                                                                                                                          
   <Searchbar mode="divided" ... />    // deprecated in M3 Expressive      

Related issue

#4978

Test plan

  • yarn typescript
  • yarn lint
  • yarn test
  • Visual verification (yarn example web):
    • Contained bar grows wider on focus; reduced-motion disables the animation
    • Input vs placeholder colors correct (input onSurface, placeholder onSurfaceVariant)
    • Divided mode: square corners + bottom divider; showDivider={false} hides it
    • Trailering icon / right item / loading states render correctly
    • Searchbar.Results renders the results list below the bar
searchbar.mp4

@MrMuzyk
MrMuzyk marked this pull request as ready for review July 9, 2026 11:27

@MikitasK MikitasK left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job 👍
just a few things comments from my side:

Comment on lines +314 to +330

return (
<Reanimated.View
style={applyFocusMargin ? containedMarginStyle : null}
testID={`${testID}-wrapper`}
>
<Surface
style={[
styles.container,
{ backgroundColor: containerColor, borderRadius },
style,
]}
testID={`${testID}-container`}
elevation={elevation}
container
theme={theme}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we apply parent-facing layout styles to this wrapper?
currently flex, position, alignSelf & similar props remain on nested Surface, so they no longer affect Searchbar relative to its parent

we could split layout & visual styles using existing splitStyles helper

Suggested change
return (
<Reanimated.View
style={applyFocusMargin ? containedMarginStyle : null}
testID={`${testID}-wrapper`}
>
<Surface
style={[
styles.container,
{ backgroundColor: containerColor, borderRadius },
style,
]}
testID={`${testID}-container`}
elevation={elevation}
container
theme={theme}
>
const [surfaceStyle, wrapperStyle] = splitStyles(flatStyle || {}, (key) => key.startsWith('margin') || ['position', 'alignSelf', 'top', 'right' ...].includes(key)
);
return (
<Reanimated.View
style={[wrapperStyle, applyFocusMargin ? containedMarginStyle : null]}
testID={`${testID}-wrapper`}
>
<Surface
style={[
styles.container,
{ backgroundColor: containerColor, borderRadius },
surfaceStyle,
]}
testID={`${testID}-container`}
elevation={elevation}
container
theme={theme}
>

Comment on lines +38 to +45
const HORIZONTAL_MARGIN_KEYS = [
'margin',
'marginHorizontal',
'marginLeft',
'marginRight',
'marginStart',
'marginEnd',
] as const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about including logical horizontal margin props here?
marginInline, marginInlineStart, marginInlineEnd are supported by RN, but currently they don’t disable built-in focus margin (RN docs)

Suggested change
const HORIZONTAL_MARGIN_KEYS = [
'margin',
'marginHorizontal',
'marginLeft',
'marginRight',
'marginStart',
'marginEnd',
] as const;
const HORIZONTAL_MARGIN_KEYS = [
'margin',
'marginHorizontal',
'marginLeft',
'marginRight',
'marginStart',
'marginEnd',
'marginInline',
'marginInlineStart',
'marginInlineEnd',
] as const;

@MikitasK MikitasK left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • updated implementation to preserve consumer layout styles on outer wrapper while keeping focus-margin animation on a separate Reanimated view
  • expanded HORIZONTAL_MARGIN_KEYS to include marginInline, marginInlineStart, marginInlineEnd
  • added regression tests & updated snapshots

Comment thread src/components/Searchbar/tokens.ts Outdated
// component (input was onSurfaceVariant, placeholder was onSurface).
input: 'onSurface',
placeholder: 'onSurfaceVariant',
leadingIcon: 'onSurfaceVariant',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

md.comp.search-bar.leading-icon.color resolves to md.sys.color.on-surface, not on-surface-variant, and the view namespace agrees (md.comp.search-view.header.leading-icon.coloron-surface). The PR fixes two of the three roles; this is the third.

Watch the knock-on. Searchbar.tsx:284 derives iconColor from leadingIconColor, and :450 reuses it for the clear button, which per md.comp.search-bar.trailing-icon.color should stay on-surface-variant. It's invisible today only because both roles resolve alike.

-                iconColor={value ? iconColor : 'rgba(255, 255, 255, 0)'}
+                iconColor={value ? trailingIconColor : 'rgba(255, 255, 255, 0)'}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +87 to +91
const styles = StyleSheet.create({
container: {
width: '100%',
},
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new results surface ships square corners. md.comp.search-view.contained.docked.results.shapemd.sys.shape.corner.medium = 12dp. (The 28dp corner.extra-large belongs to md.comp.search-view.docked.container.shape, which is the whole docked view, not the results block.)

Everything else in the component routes shape through SearchbarTokens + resolveCornerRadius, so this wants a results: 'medium' entry in tokens.ts rather than a bare style.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +100 to +108
* Search layout mode, the default value is "contained".
* - `contained` - the recommended M3 Expressive style: a rounded, elevated
* bar whose horizontal margins animate from 24dp down to 12dp on focus
* (grow-wider effect). Providing any horizontal margin via `style`
* replaces the built-in margin and disables the focus transition.
* - `divided` - a full-bleed search view with square corners and a bottom
* `Divider`. Deprecated in M3 Expressive in favor of `contained`.
*/
mode?: 'contained' | 'divided';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/6.x/docs/guides/migration.md has a ## Components section (still only TextInput) and this PR adds nothing to it. The before/after snippet already in the PR body needs to live in the guide.

Please cover both breaks. The behavioural one is the bigger surprise: bar was full-bleed, contained inserts 24dp horizontal margins, so every existing <Searchbar /> moves. And the opt-out is undiscoverable - HORIZONTAL_MARGIN_KEYS (:74-84) includes margin, so style={{ margin: 4 }} silently disables the focus animation. Your own example had to switch to marginVertical: 4 (SearchbarExample.tsx:240) for exactly that reason.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread src/components/Searchbar/Searchbar.tsx Outdated
const hasWrapperStyle = Object.keys(wrapperStyle).length > 0;

return (
<Animated.View

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is React Native's Animated, now rendered at runtime - on main the import was used only in type positions. The "preserve Searchbar layout during focus animation" commit moved transform onto the outer wrapper, and the test at Searchbar.test.tsx:75-100 drives it with an Animated.Value, so the node has to be an RN Animated.View.

House rule is Reanimated only, and #5062's own note about animated elevation staying broken on web until the move to Reanimated says which way this is meant to travel.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants