Search
Search lets someone navigate a product with a query. The search bar and the view that shows its results are one component, which is how Material 3 named them together in 2025.
Usage
A search bar carries a leading search icon, hinted search text and any trailing icons you add. Put the suggestions or results inside as children; they show once the field has focus, and the field reports them to assistive technology as a combobox. The view they open floats over the page rather than pushing it down, and closes again as soon as focus leaves it.
<script lang="ts">
import { Search } from 'noph-ui'
import Item from '#lib/list/Item.svelte'
const dishes = ['Simple Classic Tacos', 'Mexican street corn', 'Chilaquiles verdes']
let query = $state('')
const matches = (q: string) =>
q ? dishes.filter((d) => d.toLowerCase().includes(q.toLowerCase())) : dishes
</script>
<div style="width:26rem;max-width:100%">
<Search bind:value={query} placeholder="Search product">
{#each matches(query) as dish (dish)}
<Item variant="button">{dish}</Item>
{/each}
</Search>
</div>
Semantics
The container holding the results brings no role of its own, because what goes in it is a list one
time and categories, avatars and chips the next. Give it one through resultsAttributes when the children have earned it. Name a role a combobox is allowed
to own, such as listbox, and the field turns itself into that combobox, wired up with aria-expanded and aria-controls; name nothing and it stays a plain
search field with the results as ordinary content below it.
<Search placeholder="Search product" resultsAttributes={{ role: 'listbox' }}>
<Item variant="button" role="option">Simple Classic Tacos</Item>
</Search>Options a person steps through with the arrow keys are a different component: autocomplete is the full combobox, and it
handles the active option and aria-activedescendant for you.
Styles
contained, the default, keeps the field's pill shape and colour while the results
show. Material recommends it. divided squares the field off and separates it from the results
with a rule.
<script lang="ts">
import { Search } from 'noph-ui'
import Item from '#lib/list/Item.svelte'
const dishes = ['Simple Classic Tacos', 'Mexican street corn', 'Chilaquiles verdes']
let dividedQuery = $state('')
const matches = (q: string) =>
q ? dishes.filter((d) => d.toLowerCase().includes(q.toLowerCase())) : dishes
</script>
<div style="width:26rem;max-width:100%">
<Search bind:value={dividedQuery} variant="divided" placeholder="Search product">
{#each matches(dividedQuery) as dish (dish)}
<Item variant="button">{dish}</Item>
{/each}
</Search>
</div>
Trailing icons
The trailing slot takes an avatar or actions. A clear button appears next to it on its
own once there is a query.
<script lang="ts">
import { IconButton, Search } from 'noph-ui'
import { Icon } from 'noph-ui/icons'
import Item from '#lib/list/Item.svelte'
</script>
<div style="width:26rem;max-width:100%">
<Search placeholder="Search product">
{#snippet trailing()}
<IconButton title="Filter"><Icon>settings</Icon></IconButton>
{/snippet}
<Item variant="button">Simple Classic Tacos</Item>
</Search>
</div>
Layout
docked, the default, drops the results under the field, growing them to two thirds of
the screen at the most. full-screen takes over the viewport once the field has focus, which
suits a small screen.
<script lang="ts">
import { Search } from 'noph-ui'
import Item from '#lib/list/Item.svelte'
const dishes = ['Simple Classic Tacos', 'Mexican street corn', 'Chilaquiles verdes']
let fullScreenQuery = $state('')
const matches = (q: string) =>
q ? dishes.filter((d) => d.toLowerCase().includes(q.toLowerCase())) : dishes
</script>
<div style="width:26rem;max-width:100%">
<Search bind:value={fullScreenQuery} view="full-screen" placeholder="Search product">
{#each matches(fullScreenQuery) as dish (dish)}
<Item variant="button">{dish}</Item>
{/each}
</Search>
</div>
Motion
Material 3 Expressive grows the search bar when it takes focus, by shrinking the margin it keeps to its pane from 24dp to 12dp. The component owns that margin, so the growth needs nothing from you. Change how far it travels with the two custom properties, or set them both to the same value to hold the bar still.
<script lang="ts">
import { Search } from 'noph-ui'
import Item from '#lib/list/Item.svelte'
</script>
<div style="width:26rem;max-width:100%">
<Search
placeholder="Search product"
--np-search-pane-margin="4rem"
--np-search-view-margin="0rem"
>
<Item variant="button">Simple Classic Tacos</Item>
</Search>
</div>
Search app bar
Use a search app bar as an emphasised, global entry point. It is the app bar's search variant, which carries
a field in place of a headline.
<AppBar variant="search">
{#snippet leading()}
<IconButton title="Open navigation"><Icon>menu</Icon></IconButton>
{/snippet}
{#snippet search()}
<Search bind:value={query} placeholder="Search product" />
{/snippet}
</AppBar>Theming
| Custom property | Description |
|---|---|
--np-search-container-color | Background of the field and results. |
--np-search-view-background-color | Background behind a full screen contained view. |
--np-search-shape | Corner radius of the field. |
--np-search-width | Widest the field grows. 720dp. |
--np-search-pane-margin | Space the field keeps to its pane at rest. 24dp. |
--np-search-view-margin | The same space once the view is open. 12dp. |
--np-search-results-shape | Corner radius of the docked results. |
--np-search-results-max-height | How tall the docked results grow. |
--np-search-z-index | How the open view stacks over the page. |
--np-search-divider-color | The rule under a divided field. |
--np-search-focus-indicator-color | The focus ring. |
API
Renders a <div> holding the field and its results, and takes its attributes. bind:element gives you that wrapper and bind:inputElement the input.
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | '' | Bindable. The query. |
placeholder | string | '' | Hinted search text. |
expanded | boolean | false | Bindable. Whether the results are showing. Focusing the field opens it. |
variant | 'contained' | 'divided' | 'contained' | Whether the field keeps its pill while the results show. |
view | 'docked' | 'full-screen' | 'docked' | Results under the field, or covering the viewport. |
leading | Snippet | undefined | undefined | Replaces the search icon, and the back arrow shown once expanded. |
trailing | Snippet | undefined | undefined | Avatar or actions. The clear button sits beside it. |
onsearch | (value: string) => void | undefined | Called on the Enter key with the current query. |
label | string | 'Search' | Accessible name for the field. |
clearLabel | string | 'Clear search' | Accessible name for the clear button. |
backLabel | string | 'Close search' | Accessible name for the back action shown while expanded. |
inputAttributes | HTMLInputAttributes | undefined | undefined | Extra attributes for the underlying input. |
resultsAttributes | HTMLAttributes<HTMLDivElement> | undefined | undefined | Extra attributes for the results container, and where its role goes. |