Skip to content

feat(search): collapsed search #4115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/long-carrots-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@spectrum-css/search": major
---

## S2 Collapsed search field

The search component allows for a minimized state where the search field is collapsed to a button.

### Anatomy

The collapsed state consists of a single action button that has a hover, keyboard focused, and disabled state. This state is triggered by the `is-collapsed` class on the search component. When the search field is in this state, the textfield is hidden and the search button is displayed. The button can be hovered and focused, and will expand the search field when clicked.

### Usage

The collapsed state is used to reduce the amount of space taken up by the search field. It is most commonly used next a filter button to allow users to quickly search and filter content.
5 changes: 5 additions & 0 deletions components/search/dist/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
".spectrum-Search-textfield.is-focused:hover .spectrum-Search-icon",
".spectrum-Search-textfield.is-keyboardFocused .spectrum-Search-icon",
".spectrum-Search-textfield:hover .spectrum-Search-icon",
".spectrum-Search.is-collapsed",
".spectrum-Search.is-disabled .spectrum-Search-clearButton",
".spectrum-Search.is-expanded",
".spectrum-Search:lang(ja)",
".spectrum-Search:lang(ko)",
".spectrum-Search:lang(zh)"
Expand All @@ -41,6 +43,7 @@
"--mod-search-border-width",
"--mod-search-bottom-to-text",
"--mod-search-button-inline-size",
"--mod-search-collapsed-animation-duration",
"--mod-search-color-default",
"--mod-search-color-disabled",
"--mod-search-color-focus",
Expand Down Expand Up @@ -77,6 +80,7 @@
"--spectrum-search-border-width",
"--spectrum-search-bottom-to-text",
"--spectrum-search-button-inline-size",
"--spectrum-search-collapsed-animation-duration",
"--spectrum-search-color",
"--spectrum-search-color-default",
"--spectrum-search-color-disabled",
Expand All @@ -102,6 +106,7 @@
"--spectrum-search-top-to-text"
],
"global": [
"--spectrum-animation-duration-800",
"--spectrum-border-width-200",
"--spectrum-cjk-line-height-100",
"--spectrum-component-bottom-to-text-100",
Expand Down
18 changes: 18 additions & 0 deletions components/search/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
/* stylelint-disable-next-line spectrum-tools/no-unused-custom-properties -- used to assign Textfield mods */
--spectrum-search-border-color-disabled: var(--spectrum-disabled-border-color);

/* Collapsed Search */
--spectrum-search-collapsed-animation-duration: var(--spectrum-animation-duration-800);

&:lang(ja),
&:lang(zh),
&:lang(ko) {
Expand Down Expand Up @@ -149,6 +152,21 @@
.spectrum-HelpText {
margin-block-start: var(--mod-search-to-help-text, var(--spectrum-search-to-help-text));
}

/* Animation for collapsible search expansion */
&.is-collapsed {
transition: inline-size var(--mod-search-collapsed-animation-duration, var(--spectrum-search-collapsed-animation-duration)) ease-in-out;
inline-size: var(--mod-search-button-inline-size, var(--spectrum-search-block-size));
min-inline-size: var(--mod-search-button-inline-size, var(--spectrum-search-block-size));
transform-origin: left center;
}

&.is-expanded {
transition: inline-size var(--mod-search-collapsed-animation-duration, var(--spectrum-search-collapsed-animation-duration)) ease-in-out;
inline-size: var(--mod-search-inline-size, var(--spectrum-search-inline-size));
min-inline-size: auto;
transform-origin: left center;
}
}

.spectrum-Search-clearButton {
Expand Down
25 changes: 25 additions & 0 deletions components/search/stories/search.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default {
category: "Content",
},
control: "boolean",
if: { arg: "isCollapsed", eq: false },
},
helpTextLabel: {
name: "Help text (description)",
Expand All @@ -50,6 +51,16 @@ export default {
type: { summary: "string" },
category: "Content",
},
if: { arg: "isCollapsed", eq: false },
},
isCollapsed: {
name: "Collapsed",
type: { name: "boolean" },
table: {
type: { summary: "boolean" },
category: "Component",
},
control: "boolean",
},
},
args: {
Expand All @@ -62,13 +73,15 @@ export default {
showHelpText: false,
helpTextLabel: "Help text with a suggestion of what to search for",
inputValue: "",
isCollapsed: false,
},
parameters: {
actions: {
handles: [
"change .spectrum-Search-input",
"click .spectrum-Search-clearButton",
"click .spectrum-Search-icon",
"click .spectrum-Search-actionButton",
],
},
design: {
Expand Down Expand Up @@ -141,6 +154,18 @@ WithValue.parameters = {
};
WithValue.storyName = "With value and clear button";

/**
* A search field can be collapsed to show only the search button. This is useful when there is limited space available. It is most commonly used next a filter button to allow users to quickly search and filter content.
*/
export const Collapsed = Template.bind({});
Collapsed.args = {
isCollapsed: true,
};
Collapsed.tags = ["!dev"];
Collapsed.parameters = {
chromatic: { disableSnapshot: true },
};

/**
* The medium size is the default and most frequently used option. Use the other sizes sparingly; they should be used to create a hierarchy of importance within the page.
*/
Expand Down
6 changes: 6 additions & 0 deletions components/search/stories/search.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const SearchGroup = Variants({
inputValue: "What should we search for?",
withStates: false,
},
{
testHeading: "Collapsed",
isCollapsed: true,
}
],
stateData: [
{
Expand All @@ -30,11 +34,13 @@ export const SearchGroup = Variants({
{
testHeading: "Focused",
isFocused: true,
ignore: ["Collapsed"],
},
{
testHeading: "Focused + hovered",
isFocused: true,
isHovered: true,
ignore: ["Collapsed"],
},
{
testHeading: "Keyboard focused",
Expand Down
66 changes: 45 additions & 21 deletions components/search/stories/template.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Template as ActionButton } from "@spectrum-css/actionbutton/stories/template.js";
import { Template as ClearButton } from "@spectrum-css/clearbutton/stories/template.js";
import { Template as HelpText } from "@spectrum-css/helptext/stories/template.js";
import { Container } from "@spectrum-css/preview/decorators";
Expand All @@ -19,7 +20,9 @@ export const Template = ({
size = "m",
showHelpText = false,
helpTextLabel = "",
isCollapsed = false,
} = {}, context = {}) => {
const { updateArgs } = context;
return html`
<form
class=${classMap({
Expand All @@ -28,38 +31,59 @@ export const Template = ({
typeof size !== "undefined" && size !== "m",
"is-disabled": isDisabled,
"is-keyboardFocused": isKeyboardFocused,
"is-collapsed": isCollapsed,
"is-expanded": !isCollapsed,
...customClasses.reduce((a, c) => ({ ...a, [c]: true }), {}),
})}
aria-label="Search"
>
${TextField({
isDisabled,
size,
customClasses: [
`${rootClass}-textfield`,
isFocused && "is-focused",
isKeyboardFocused && "is-keyboardFocused",
isHovered && "is-hover"
],
iconName: "Search",
setName: "workflow",
type: "search",
placeholder: "Search",
name: "search",
customInputClasses: [`${rootClass}-input`],
customIconClasses: [`${rootClass}-icon`],
autocomplete: false,
value: inputValue,
}, context)}
${when(inputValue, () =>
${when(isCollapsed, () =>
ActionButton({
iconName: "Search",
isDisabled,
size,
isFocused: !isDisabled && (isFocused || isKeyboardFocused),
isQuiet: true,
customClasses: [
`${rootClass}-actionButton`,
isHovered && "is-hover",
isDisabled && "is-disabled",
Copy link
Collaborator

Choose a reason for hiding this comment

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

My only suggestion is to make sure we can't keyboard focus to this button when it's disabled!

Screenshot 2025-08-14 at 6 55 16 PM

I'm not sure if this is a property thing? It certainly looks like we're passing the disabled arg through correctly- I see it on the search AND on the action button, but maybe we need to revisit the disabled+keyboard focus styles.

],
onclick: () => {
updateArgs({ isCollapsed: !isCollapsed });
},
}, context)
)}
${when(!isCollapsed, () =>
TextField({
isDisabled,
size,
customClasses: [
`${rootClass}-textfield`,
isFocused && "is-focused",
isKeyboardFocused && "is-keyboardFocused",
isHovered && "is-hover"
],
iconName: "Search",
setName: "workflow",
type: "search",
placeholder: "Search",
name: "search",
customInputClasses: [`${rootClass}-input`],
customIconClasses: [`${rootClass}-icon`],
autocomplete: false,
value: inputValue,
}, context)
)}
${when(inputValue && !isCollapsed, () =>
ClearButton({
isDisabled,
size,
customClasses: [`${rootClass}-clearButton`],
isFocusable: false,
}, context)
)}
${when(showHelpText, () =>
${when(showHelpText && !isCollapsed, () =>
HelpText({
text: helpTextLabel,
size,
Expand Down
Loading