+
+
+
+@code
+{
+ IEnumerable Selected { get; set; } = [];
+
+ /// Search method called when the user types in the input or opens the options list.
+ /// A new list of MyUser is assigned with new object instances.
+ /// The component will use the OptionSelectedComparer to check if any of the new items are already selected.
+ Task OnSearchAsync(OptionsSearchEventArgs e)
+ {
+ e.Items = [
+ new MyUser(1, "Marvin Klein"),
+ new MyUser(2, "Denis Voituron"),
+ ];
+
+ return Task.CompletedTask;
+ }
+
+ record MyUser(int UserId, string Name);
+
+ class MyComparer : IEqualityComparer
+ {
+ public static readonly MyComparer Instance = new();
+
+ public bool Equals(MyUser? x, MyUser? y) => x?.UserId == y?.UserId;
+
+ public int GetHashCode(MyUser obj) => obj.UserId.GetHashCode();
+ }
+}
\ No newline at end of file
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Autocomplete/Examples/AutocompleteCustomized.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Autocomplete/Examples/AutocompleteCustomized.razor
new file mode 100644
index 0000000000..4a2a88f191
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Autocomplete/Examples/AutocompleteCustomized.razor
@@ -0,0 +1,82 @@
+@using static FluentUI.Demo.SampleData.Olympics2024
+
+
+
+
+@code
+{
+ IEnumerable SelectedCountries { get; set; } = [];
+
+ Country? SelectedCountry
+ {
+ get => SelectedCountries.FirstOrDefault() ?? default;
+ set => SelectedCountries = value is not null ? [value] : [];
+ }
+
+ Task OnSearchAsync(OptionsSearchEventArgs e)
+ {
+ e.Items = Countries.Where(i => i.Name.StartsWith(e.Text, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(i => i.Name);
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Autocomplete/FluentAutocomplete.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Autocomplete/FluentAutocomplete.md
new file mode 100644
index 0000000000..cef71fefbe
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Autocomplete/FluentAutocomplete.md
@@ -0,0 +1,67 @@
+---
+title: Autocomplete
+route: /Lists/Autocomplete
+---
+
+# Autocomplete
+
+An **Autocomplete** component is a text input that provides real-time suggestions as the user types.
+It combines a free-text input with a filtered list of options, allowing users to either select from the suggestions or type their own value.
+
+This is particularly useful when the list of options is large, as the user can narrow down the list op options without needing to scroll through all available items.
+
+By default, the `FluentAutocomplete` component compares search results by instance with its internal selected items.
+You can control this behavior by providing the `OptionSelectedComparer` parameter.
+
+> **Note:** Accessibility requirements are not yet implemented for this component.
+
+## Keyboard interaction
+
+| Key | Behavior |
+|---|---|
+| **Type text** | Filters the list of options and triggers the `OnSearchAsync` method to fetch matching results. |
+| **Arrow Down / Arrow Up** | Opens the suggestion list and navigates through the items in the suggestion list. |
+| **Enter** | Selects the currently highlighted item. |
+| **Backspace** | Deletes the most recently selected item (in multi-select mode). |
+| **Escape** | Closes the suggestion list without selecting an item. |
+
+
+
+## Default
+
+A basic autocomplete that filters a list of countries as the user types.
+Multiple items can be selected, and one option is disabled (`OptionDisabled`).
+
+{{ AutocompleteDefault }}
+
+## Single item (Multiple=false)
+
+Set the `Multiple` parameter to `false` to restrict the selection to a single item.
+In this mode, the selected value replaces the input text and no tags are displayed.
+
+{{ AutocompleteMultipleFalse }}
+
+## Customized options
+
+Demonstrates advanced features: a custom `OptionTemplate` to render each option with a flag, a progress indicator during async search,
+a configurable max dropdown height, and a max width for selected items.
+
+{{ AutocompleteCustomized }}
+
+## Different object instances from search result
+
+When the `OnOptionsSearch` method returns **new object instances** on each call (e.g. from an API or database query),
+the component cannot match them to already-selected items by **reference**.
+
+Use the `OptionSelectedComparer` parameter to provide a custom `IEqualityComparer` that compares items by a unique key (such as an ID)
+instead of by reference. Without this, previously selected items may not appear as checked in the refreshed list.
+
+{{ AutocompleteComparer }}
+
+## API FluentAutocomplete
+
+{{ API Type=FluentAutocomplete }}
+
+## Migrating to v5
+
+{{ INCLUDE File=MigrationFluentAutocomplete }}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Listbox/DebugPages/DebugList.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Listbox/DebugPages/DebugList.razor
index f0691d9f2d..d11a77cb34 100644
--- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Listbox/DebugPages/DebugList.razor
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/List/Listbox/DebugPages/DebugList.razor
@@ -12,7 +12,7 @@
TOption="string"
TValue="string"
@bind-SelectedItems="@SelectedItems"
- Multiple="true" />
+ Multiple="false" />
Yellow, Purple, Cyan
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentAutocomplete.md b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentAutocomplete.md
new file mode 100644
index 0000000000..88de6b037e
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentAutocomplete.md
@@ -0,0 +1,102 @@
+---
+title: Migration FluentAutocomplete
+route: /Migration/Autocomplete
+hidden: true
+---
+
+### Type parameter change 💥
+
+The component now requires **two** type parameters: `TOption` and `TValue`.
+
+```csharp
+// v4
+
+
+// v5
+
+```
+
+### Renamed parameters 💥
+
+- `@bind-SelectedOptions` → `@bind-SelectedItems`
+- `@bind-SelectedOption` → Use `Multiple="false"` with `@bind-SelectedItems`
+- `Appearance` (`FluentInputAppearance`) → `InputAppearance` (`TextInputAppearance`)
+- `OptionComparer` → `OptionSelectedComparer`
+- `ValueText` / `ValueTextChangedAsync` → `@bind-Value`
+
+### Removed parameters 💥
+
+- `AutoComplete` — browser autocomplete attribute, no longer exposed.
+- `Position` (`SelectPosition?`) — popup positioning is now handled internally.
+- `OptionStyle` / `OptionClass` — use `OptionTemplate` to customize option rendering.
+- `TitleScrollToPrevious` / `TitleScrollToNext` — horizontal scroll navigation has been removed.
+- `ShowOverlayOnEmptyResults` — overlay behavior has been removed.
+- `Virtualize` / `ItemSize` — virtualization support has not yet implemented.
+- `SelectValueOnTab` — tab key behavior has been removed.
+- `KeepOpen` — dropdown close behavior is now managed internally.
+
+### New parameters
+
+- `TValue` — second type parameter for the value type.
+- `MaxSelectedWidth` (`string?`) — maximum width of each selected item badge.
+- `ShowDismiss` (`bool`) — controls whether the Search/Clear icon button is displayed. Default is `true`.
+- `OptionValue` (`Func?`) — function to extract the value from an option.
+- `OptionValueToString` (`Func?`) — function to convert a value to string.
+- Various inherited input parameters: `Message`, `MessageTemplate`, `MessageState`, `MessageIcon`, `LabelPosition`, `LabelWidth`, `Margin`, `Padding`, `Tooltip`.
+
+### HeaderContent / FooterContent context type change 💥
+
+The context type for `HeaderContent` and `FooterContent` has been renamed
+from `HeaderFooterContent` to `AutocompleteHeaderFooterContent`.
+The properties remain the same (`Items` and `InProgress`).
+
+### Single selection mode 💥
+
+In v4, single selection used a separate `@bind-SelectedOption` binding.
+In v5, use `Multiple="false"` with `@bind-SelectedItems`.
+
+```csharp
+// v4
+
+
+// v5
+
+
+@code
+{
+ Country? SelectedCountry
+ {
+ get => SelectedCountries.FirstOrDefault() ?? default;
+ set => SelectedCountries = value is not null ? [value] : [];
+ }
+}
+```
+
+### Automatic height growth
+
+In v4, the component used horizontal scroll navigation (`FluentFlipper`) when selected items exceeded the available width.
+In v5, this horizontal navigation has been removed, and the component **grows vertically** to display all selected items:
+set the `MaxAutoHeight` parameter to `unset` or a specific value.
+You can also use `MaxSelectedWidth` to truncate long selected item labels, reducing the horizontal space each badge occupies.
+This may break existing layouts if you relied on the fixed-height behavior.
+
+### Migrating to v5
+
+| v4 | v5 |
+|---|---|
+| `TOption` only | `TOption` + `TValue` |
+| `@bind-SelectedOptions` | `@bind-SelectedItems` |
+| `@bind-SelectedOption` | `Multiple="false"` + `@bind-SelectedItems` |
+| `Appearance="FluentInputAppearance.Outline"` | `InputAppearance="TextInputAppearance.Outline"` |
+| `OptionComparer` | `OptionSelectedComparer` |
+| `ValueText` / `ValueTextChangedAsync` | `@bind-Value` |
+| `HeaderFooterContent` | `AutocompleteHeaderFooterContent` |
+| `SelectValueOnTab="true"` | _(removed)_ |
+| `KeepOpen="true"` | _(removed)_ |
+
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentList.md b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentList.md
index 0d97a3baf1..a5e31294c3 100644
--- a/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentList.md
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/GetStarted/Migration/MigrationFluentList.md
@@ -1,26 +1,9 @@
---
-title: Migration FluentList (Autocomplete, Combobox, Listbox)
+title: Migration FluentList (Combobox, Listbox, Select)
route: /Migration/List
hidden: true
---
-- ### FluentAutocomplete removed 💥
-
- `FluentAutocomplete` has been **removed** in V5.
- Use `FluentCombobox` with the `FreeOption` parameter as a replacement for autocomplete behavior.
-
- ```xml
-
-
-
-
-
- Custom option template
-
- ```
-
- ### Two type parameters required 💥
All list components now require two type parameters: `TOption` and `TValue`.
diff --git a/src/Core.Scripts/src/Components/List/FluentAutocomplete.ts b/src/Core.Scripts/src/Components/List/FluentAutocomplete.ts
new file mode 100644
index 0000000000..d244bcd6e3
--- /dev/null
+++ b/src/Core.Scripts/src/Components/List/FluentAutocomplete.ts
@@ -0,0 +1,218 @@
+import { DropdownOption, TextInput } from "@fluentui/web-components";
+
+export namespace Microsoft.FluentUI.Blazor.Components.Autocomplete {
+
+ /**
+ * Initializes the FluentAutocomplete component by attaching keyboard navigation event listeners
+ * to the input element and managing the popover state for option selection.
+ * @param id The ID of the input element to initialize.
+ */
+ export function initialize(id: string) {
+ const input = document.getElementById(id) as TextInput;
+ if (!input) return;
+
+ detectWrappedItems(input);
+ new AutocompleteKeyboardNav(id, input);
+ }
+
+ /**
+ * Sets focus to the input element of the FluentAutocomplete component, allowing users to start typing immediately.
+ * @param id The ID of the input element to focus.
+ */
+ export function setFocus(id: string) {
+ const input = document.getElementById(id) as TextInput;
+ if (!input) return;
+
+ input.focus();
+
+ // Move the cursor to the end of the input value
+ const control = (input as any).control as HTMLInputElement;
+ if (control) {
+ const len = control.value.length;
+ control.setSelectionRange(len, len);
+ }
+ }
+
+ /**
+ * Detects if the items in the start slot of the autocomplete input are wrapped to multiple lines and sets an attribute accordingly.
+ */
+ function detectWrappedItems(input: TextInput): void {
+
+ const startSlot = input.querySelector("div[slot='start'] > div") as HTMLElement;
+ if (!startSlot) return;
+
+ const observer = new ResizeObserver(entries => {
+ for (const entry of entries) {
+ if (isFlexWrapped(input, startSlot)) {
+ input.setAttribute("items-wrapped", "true");
+ } else {
+ input.removeAttribute("items-wrapped");
+ }
+ }
+ });
+
+ observer.observe(startSlot);
+ }
+
+ /**
+ * Returns true if the items in the container are wrapped to multiple lines.
+ */
+ function isFlexWrapped(input: TextInput, container: HTMLElement): boolean {
+ const items = Array.from(container.children) as HTMLElement[];
+ if (items.length < 2) return false;
+
+ if (input.getAttribute("dismiss") === "hidden") return false;
+ if (input.hasAttribute("items-wrapped")) return true;
+
+ const firstTop = items[0].offsetTop;
+ return items.some(item => item.offsetTop !== firstTop);
+ }
+
+ /**
+ * Handles keyboard navigation for the FluentAutocomplete component.
+ * - ArrowDown: Moves hover to the next option.
+ * - ArrowUp: Moves hover to the previous option.
+ * - Enter: Selects the currently hovered option.
+ */
+ class AutocompleteKeyboardNav {
+
+ private inputId: string;
+ private input: TextInput;
+
+ /**
+ * Initializes the keyboard navigation for the autocomplete input.
+ */
+ constructor(inputId: string, input: TextInput) {
+ this.inputId = inputId;
+ this.input = input;
+
+ const popover = this.getPopover();
+ if (!popover) throw new Error(`Popover not found for input with id ${inputId}`);
+ this.Popover = popover as IFluentPopover;
+
+ this.input.addEventListener('keydown', this.keydownHandler);
+ this.input.addEventListener('input', this.inputChangeHandler);
+ this.Popover.addEventListener('toggle', this.popoverToggleHandler);
+ }
+
+ private Popover: IFluentPopover;
+
+ /**
+ * Handles keydown events on the autocomplete input to manage option hovering and selection.
+ */
+ private keydownHandler = (e: KeyboardEvent): void => {
+
+ const options = this.getOptions();
+ const currentIndex = options.findIndex(o => o.hasAttribute('hovered'));
+
+ switch (e.key) {
+
+ case 'ArrowDown': {
+ e.preventDefault();
+
+ if (!this.isPopoverOpen()) this.Popover.showPopover();
+
+ const nextIndex = currentIndex < options.length - 1 ? currentIndex + 1 : options.length - 1;
+ this.setHover(options, nextIndex);
+
+ break;
+ }
+
+ case 'ArrowUp': {
+ e.preventDefault();
+
+ if (!this.isPopoverOpen()) this.Popover.showPopover();
+
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : 0;
+ this.setHover(options, prevIndex);
+
+ break;
+ }
+
+ case 'Enter': {
+ if (currentIndex >= 0 && this.isPopoverOpen()) {
+ e.preventDefault();
+ options[currentIndex].click();
+
+ // Close the popover after selection
+ this.Popover.closePopover();
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns the popover element associated with the autocomplete input.
+ */
+ private getPopover(): HTMLElement | null {
+ return document.querySelector(`fluent-popover-b[anchor-id="${this.inputId}"]`);
+ }
+
+ /**
+ * Returns true if the popover is currently open, false otherwise.
+ */
+ private isPopoverOpen(): boolean {
+ return this.Popover.hasAttribute('opened') && (this.Popover.getAttribute('opened') === 'true' || this.Popover.getAttribute('opened') === '' || this.Popover.getAttribute('opened') === null);
+ }
+
+ /**
+ * Returns an array of enabled options within the popover.
+ */
+ private getOptions(): DropdownOption[] {
+ return Array.from(this.Popover.querySelectorAll('fluent-option:not([disabled])'));
+ }
+
+ /**
+ * Sets the hovered attribute on the specified option.
+ */
+ private setHover(options: DropdownOption[], index: number): void {
+ if (index < 0 || index >= options.length) return;
+ this.clearAllHovers();
+ options[index].setAttribute('hovered', '');
+ options[index].tabIndex = 0;
+ }
+
+ /**
+ * Sets the hover on the first option in the popover.
+ */
+ private setHoverFirstOption(): void {
+ const options = this.getOptions();
+ if (options.length === 0) return;
+ this.setHover(options, 0);
+ }
+
+ /**
+ * Clears the hovered attribute from all options in the popover.
+ */
+ private clearAllHovers(): void {
+ this.Popover.querySelectorAll('fluent-option[hovered]').forEach((opt: Element) => {
+ const option = opt as DropdownOption;
+ option.removeAttribute('hovered');
+ option.tabIndex = -1;
+ });
+ }
+
+ /**
+ * Clears all hovers when the input value changes to ensure the hover state is reset when the user types.
+ */
+ private inputChangeHandler = (): void => {
+ this.setHoverFirstOption();
+ }
+
+ /**
+ * Hovers the first option when the popover is opened.
+ */
+ private popoverToggleHandler = (e: Event): void => {
+ if ((e as CustomEvent).detail?.newState === 'open') {
+ this.setHoverFirstOption();
+ }
+ }
+
+ }
+
+ interface IFluentPopover extends HTMLElement {
+ showPopover(): void;
+ closePopover(): void;
+ }
+}
diff --git a/src/Core.Scripts/src/Components/List/ListBoxContainer.ts b/src/Core.Scripts/src/Components/List/ListBoxContainer.ts
index a4312b960e..e6be5a5aec 100644
--- a/src/Core.Scripts/src/Components/List/ListBoxContainer.ts
+++ b/src/Core.Scripts/src/Components/List/ListBoxContainer.ts
@@ -43,6 +43,7 @@ export namespace Microsoft.FluentUI.Blazor.Components.ListBoxContainer {
private isInitialized: boolean = false;
private container: HTMLElement;
private listbox: FluentUIComponents.Listbox;
+ private pendingSelectedOptionsChange: boolean = false;
/**
* Initializes a new instance of the ListboxExtended class.
@@ -70,7 +71,7 @@ export namespace Microsoft.FluentUI.Blazor.Components.ListBoxContainer {
this.listbox.multiple = (this.container.hasAttribute('multiple')) ?? false;
// Set initial selected options based on the current state
- if (this.listbox.multiple) {
+ if (this.listbox.multiple && this.listbox.options) {
const selectedIds = this.listbox.selectedOptions.map(option => option.id);
for (let i = 0; i < this.listbox.options.length; i++) {
const option = this.listbox.options[i];
@@ -252,8 +253,12 @@ export namespace Microsoft.FluentUI.Blazor.Components.ListBoxContainer {
});
}
- if (hasSelectedOptionsChanged) {
- this.raiseSelectedOptionsChangeEvent();
+ if (hasSelectedOptionsChanged && !this.pendingSelectedOptionsChange) {
+ this.pendingSelectedOptionsChange = true;
+ queueMicrotask(() => {
+ this.pendingSelectedOptionsChange = false;
+ this.raiseSelectedOptionsChangeEvent();
+ });
}
});
diff --git a/src/Core.Scripts/src/ExportedMethods.ts b/src/Core.Scripts/src/ExportedMethods.ts
index 4eb065ff0e..d15ac2e5ee 100644
--- a/src/Core.Scripts/src/ExportedMethods.ts
+++ b/src/Core.Scripts/src/ExportedMethods.ts
@@ -10,6 +10,7 @@ import { Microsoft as FluentTextMaskedFile } from './Components/TextInput/TextMa
import { Microsoft as FluentTextInput } from './Components/TextInput/TextInput';
import { Microsoft as FluentOverlayFile } from './Components/Overlay/FluentOverlay';
import { Microsoft as FluentListBoxContainerFile } from './Components/List/ListBoxContainer';
+import { Microsoft as FluentAutocompleteFile } from './Components/List/FluentAutocomplete';
export namespace Microsoft.FluentUI.Blazor.ExportedMethods {
@@ -40,6 +41,7 @@ export namespace Microsoft.FluentUI.Blazor.ExportedMethods {
(window as any).Microsoft.FluentUI.Blazor.Components.TextInput = FluentTextInput.FluentUI.Blazor.Components.TextInput;
(window as any).Microsoft.FluentUI.Blazor.Components.Overlay = FluentOverlayFile.FluentUI.Blazor.Components.Overlay;
(window as any).Microsoft.FluentUI.Blazor.Components.ListBoxContainer = FluentListBoxContainerFile.FluentUI.Blazor.Components.ListBoxContainer;
+ (window as any).Microsoft.FluentUI.Blazor.Components.Autocomplete = FluentAutocompleteFile.FluentUI.Blazor.Components.Autocomplete;
// [^^^ Add your other exported methods before this line ^^^]
}
diff --git a/src/Core/Components/Base/FluentInputImmediateBase.cs b/src/Core/Components/Base/FluentInputImmediateBase.cs
index 6b61de091a..52312f4f2f 100644
--- a/src/Core/Components/Base/FluentInputImmediateBase.cs
+++ b/src/Core/Components/Base/FluentInputImmediateBase.cs
@@ -26,10 +26,11 @@ protected FluentInputImmediateBase(LibraryConfiguration configuration) : base(co
public bool Immediate { get; set; } = false;
///
- /// Gets or sets the delay, in milliseconds, before to raise the event.
+ /// Gets or sets the delay, in milliseconds, before to raise the event.
+ /// Default is 200 milliseconds.
///
[Parameter]
- public int ImmediateDelay { get; set; } = 0;
+ public int ImmediateDelay { get; set; } = 200;
///
/// Handler for the OnInput event, with an optional delay to avoid to raise the event too often.
diff --git a/src/Core/Components/Icons/FluentIcon.razor b/src/Core/Components/Icons/FluentIcon.razor
index 06e29159d5..47629dcaf9 100644
--- a/src/Core/Components/Icons/FluentIcon.razor
+++ b/src/Core/Components/Icons/FluentIcon.razor
@@ -15,6 +15,8 @@
aria-hidden="@(Focusable ? null : "true")"
@onkeydown="@OnKeyDownAsync"
@onclick="@OnClickHandlerAsync"
+ @onclick:stopPropagation="@OnClickStopPropagation"
+ @onclick:preventDefault="@OnClickPreventDefault"
@attributes="@AdditionalAttributes">
@if (!string.IsNullOrEmpty(Title))
{
@@ -35,7 +37,9 @@ else
role="@AdditionalAttributes.GetValueIfNoAdditionalAttribute("role", "button", when: () => Focusable)"
@attributes="@AdditionalAttributes"
@onkeydown="@OnKeyDownAsync"
- @onclick="@OnClickHandlerAsync">
+ @onclick="@OnClickHandlerAsync"
+ @onclick:stopPropagation="@OnClickStopPropagation"
+ @onclick:preventDefault="@OnClickPreventDefault">
@((MarkupString)@_icon.Content)
}
diff --git a/src/Core/Components/Icons/FluentIcon.razor.cs b/src/Core/Components/Icons/FluentIcon.razor.cs
index 37e0611c8a..5294874261 100644
--- a/src/Core/Components/Icons/FluentIcon.razor.cs
+++ b/src/Core/Components/Icons/FluentIcon.razor.cs
@@ -86,6 +86,18 @@ public Icon Value
[Parameter]
public EventCallback OnClick { get; set; }
+ ///
+ /// Gets or sets whether the click event should stop propagation.
+ ///
+ [Parameter]
+ public bool OnClickStopPropagation { get; set; }
+
+ ///
+ /// Gets or sets whether the click event should prevent the default action.
+ ///
+ [Parameter]
+ public bool OnClickPreventDefault { get; set; }
+
///
/// Gets or sets whether the icon is focusable (adding tab-index="0" and role="button"),
/// allows the icon to be focused sequentially (generally with the Tab key).
diff --git a/src/Core/Components/List/AutocompleteHeaderFooterContent.cs b/src/Core/Components/List/AutocompleteHeaderFooterContent.cs
new file mode 100644
index 0000000000..8f2d3875ed
--- /dev/null
+++ b/src/Core/Components/List/AutocompleteHeaderFooterContent.cs
@@ -0,0 +1,30 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// This class represents the content to be displayed in the header or footer of the FluentAutocomplete component.
+///
+///
+public class AutocompleteHeaderFooterContent
+{
+ ///
+ internal AutocompleteHeaderFooterContent(IEnumerable? items, bool inProgress)
+ {
+ Items = items ?? [];
+ InProgress = inProgress;
+ }
+
+ ///
+ /// Gets a value indicating whether the operation is currently in progress.
+ /// Set to true to refresh this property in the header or footer.
+ ///
+ public bool InProgress { get; init; }
+
+ ///
+ /// Gets the items to display in the header or footer.
+ ///
+ public IEnumerable Items { get; init; }
+}
diff --git a/src/Core/Components/List/FluentAutocomplete.razor b/src/Core/Components/List/FluentAutocomplete.razor
new file mode 100644
index 0000000000..87bcabac13
--- /dev/null
+++ b/src/Core/Components/List/FluentAutocomplete.razor
@@ -0,0 +1,199 @@
+@namespace Microsoft.FluentUI.AspNetCore.Components
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+@inherits FluentListBase
+@typeparam TOption
+@typeparam TValue
+
+
+
+
+ DisplayFilteredOptionsAsync(showWhenInputIsEmpty: true))"
+ @bind-Value="@_textInput"
+ @bind-Value:after="@DisplayFilteredOptionsAsync">
+ @* Selected items *@
+
+ @if (Multiple)
+ {
+
+ @* Show selected items as badges with an "x" button to remove them.
+ This is shown when the listbox is closed, when it's open the selected items are highlighted
+ in the listbox and we don't show the badges in the input. *@
+ @if (_internalSelectedItems is not null)
+ {
+ @foreach (var item in _internalSelectedItems)
+ {
+ if (SelectedOptionTemplate is null)
+ {
+
+ @GetOptionText(item)
+
+
+ }
+ else
+ {
+ @SelectedOptionTemplate(item)
+ }
+ }
+ }
+
+ }
+ else
+ {
+ @if (_internalSelectedItem is not null)
+ {
+