diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/Examples/HighlighterDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/Examples/HighlighterDefault.razor
new file mode 100644
index 0000000000..4e36904f0c
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/Examples/HighlighterDefault.razor
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+@code
+{
+ static string Text = SampleData.Text.GenerateLoremIpsum();
+ string Highlight = "Lorem";
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/Examples/HighlighterDelimiters.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/Examples/HighlighterDelimiters.razor
new file mode 100644
index 0000000000..3590ea0836
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/Examples/HighlighterDelimiters.razor
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+@code
+{
+ static string Text = SampleData.Text.GenerateLoremIpsum();
+ string Highlight = "Lore, ips";
+ bool Styled = false;
+ bool UntilNextBoundary = false;
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/FluentHighlighter.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/FluentHighlighter.md
new file mode 100644
index 0000000000..18249dcdb6
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Highlighter/FluentHighlighter.md
@@ -0,0 +1,33 @@
+---
+title: Highlighter
+route: /Highlighter
+---
+
+# Highlighter
+
+A component which highlights words or phrases within text.
+The highlighter can be used in combination with any other component.
+
+## General usage
+
+{{ HighlighterDefault }}
+
+## Multiple Highlights
+
+In addition to `HighlightedText` parameter which accepts a single text fragment in the form of an string,
+the `Delimiters` parameter define a list of chars which can be used to highlight several text fragments.
+See this example where `Delimiters=" ,;"` where you can use space, comma and semicolon to hihlight the search text.
+
+Set the `UntilNextBoundary="true"` parameter if you want to highlight the text until the next regex boundary occurs.
+This is useful when you want to highlight a word and all the text until the next **space**.
+In this example, the `HighlightedText="Lore, ips"` and the component will highlight the text until the next boundary which is a **space**.
+
+{{ HighlighterDelimiters }}
+
+## API FluentHighlighter
+
+{{ API Type=FluentHighlighter }}
+
+## Migrating to v5
+
+No changes
diff --git a/src/Core/Components/Highlighter/FluentHighlighter.razor b/src/Core/Components/Highlighter/FluentHighlighter.razor
new file mode 100644
index 0000000000..c2721c965f
--- /dev/null
+++ b/src/Core/Components/Highlighter/FluentHighlighter.razor
@@ -0,0 +1,15 @@
+@namespace Microsoft.FluentUI.AspNetCore.Components
+@using System.Text.RegularExpressions
+@inherits FluentComponentBase
+
+@foreach (var fragment in _fragments.Span)
+{
+ if (!string.IsNullOrWhiteSpace(_regex) && Regex.IsMatch(fragment, _regex, CaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase))
+ {
+ @fragment
+ }
+ else
+ {
+ @fragment
+ }
+}
diff --git a/src/Core/Components/Highlighter/FluentHighlighter.razor.cs b/src/Core/Components/Highlighter/FluentHighlighter.razor.cs
new file mode 100644
index 0000000000..adab983e01
--- /dev/null
+++ b/src/Core/Components/Highlighter/FluentHighlighter.razor.cs
@@ -0,0 +1,64 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.Components;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// A component which highlights words or phrases within text.
+///
+public partial class FluentHighlighter : FluentComponentBase
+{
+ private Memory _fragments;
+ private string _regex = string.Empty;
+
+ ///
+ protected string? ClassValue => DefaultClassBuilder
+ .Build();
+
+ ///
+ protected string? StyleValue => DefaultStyleBuilder
+ .Build();
+
+ ///
+ /// Gets or sets a value indicating whether the highlighted text is case sensitive.
+ ///
+ [Parameter]
+ public bool CaseSensitive { get; set; } = false;
+
+ ///
+ /// Gets or sets the fragment of text to be highlighted.
+ ///
+ [Parameter]
+ public string HighlightedText { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the whole text in which a fragment will be highlighted.
+ ///
+ [Parameter]
+ public string Text { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the list of delimiters chars. Example: " ,;".
+ ///
+ [Parameter]
+ public string Delimiters { get; set; } = string.Empty;
+
+ ///
+ /// If true, highlights the text until the next regex boundary.
+ ///
+ [Parameter]
+ public bool UntilNextBoundary { get; set; }
+
+ ///
+ protected override void OnParametersSet()
+ {
+ var highlightedTexts = string.IsNullOrEmpty(Delimiters)
+ ? [HighlightedText]
+ : HighlightedText.Split(Delimiters.ToCharArray());
+
+ _fragments = HighlighterSplitter.GetFragments(Text, highlightedTexts, out _regex, CaseSensitive, UntilNextBoundary);
+ }
+}
diff --git a/src/Core/Components/Highlighter/HighlighterSplitter.cs b/src/Core/Components/Highlighter/HighlighterSplitter.cs
new file mode 100644
index 0000000000..7ea5834a39
--- /dev/null
+++ b/src/Core/Components/Highlighter/HighlighterSplitter.cs
@@ -0,0 +1,136 @@
+// ------------------------------------------------------------------------
+// MIT License - Copyright (c) Microsoft Corporation. All rights reserved.
+// ------------------------------------------------------------------------
+
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Splits the text into fragments, according to the text to be highlighted
+///
+/// Inspired from https://github.com/MudBlazor
+internal sealed class HighlighterSplitter
+{
+ private static readonly TimeSpan _regExMatchTimeout = TimeSpan.FromMilliseconds(100);
+ private const string NextBoundary = ".*?\\b";
+
+ private static StringBuilder? _stringBuilderCached;
+
+ ///
+ /// Splits the text into fragments, according to the
+ /// text to be highlighted
+ ///
+ /// The whole text
+ /// The texts to be highlighted
+ /// Regex expression that was used to split fragments.
+ /// Whether it's case sensitive or not
+ /// If true, splits until the next regex boundary
+ ///
+ internal static Memory GetFragments(
+ string text,
+ IEnumerable highlightedTexts,
+ out string regex,
+ bool caseSensitive = false,
+ bool untilNextBoundary = false)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ regex = string.Empty;
+ return Memory.Empty;
+ }
+
+ var builder = GetStringBuilder();
+ regex = BuildRegexPattern(builder, highlightedTexts, untilNextBoundary);
+
+ if (string.IsNullOrEmpty(regex))
+ {
+ return new string[] { text };
+ }
+
+ var splits = SplitText(text, regex, caseSensitive);
+ return FilterEmptyFragments(splits);
+ }
+
+ ///
+ private static StringBuilder GetStringBuilder()
+ {
+ return Interlocked.Exchange(ref _stringBuilderCached, value: null) ?? new StringBuilder();
+ }
+
+ ///
+ private static string BuildRegexPattern(StringBuilder builder, IEnumerable highlightedTexts, bool untilNextBoundary)
+ {
+ builder.Append("((?:");
+ var hasAtLeastOnePattern = false;
+
+ if (highlightedTexts is not null)
+ {
+ foreach (var substring in highlightedTexts)
+ {
+ if (string.IsNullOrEmpty(substring))
+ {
+ continue;
+ }
+
+ if (hasAtLeastOnePattern)
+ {
+ builder.Append(")|(?:");
+ }
+
+ AppendPattern(builder, substring, untilNextBoundary);
+ hasAtLeastOnePattern = true;
+ }
+ }
+
+ if (hasAtLeastOnePattern)
+ {
+ builder.Append("))");
+ }
+ else
+ {
+ builder.Clear();
+ _stringBuilderCached = builder;
+ return string.Empty;
+ }
+
+ var regex = builder.ToString();
+ builder.Clear();
+ _stringBuilderCached = builder;
+ return regex;
+ }
+
+ ///
+ private static void AppendPattern(StringBuilder builder, string value, bool untilNextBoundary)
+ {
+ value = Regex.Escape(value);
+ builder.Append(value);
+ if (untilNextBoundary)
+ {
+ builder.Append(NextBoundary);
+ }
+ }
+
+ ///
+ private static string[] SplitText(string text, string regex, bool caseSensitive)
+ {
+ return Regex.Split(text, regex, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase, _regExMatchTimeout);
+ }
+
+ ///
+ private static Memory FilterEmptyFragments(string[] splits)
+ {
+ var length = 0;
+ for (var i = 0; i < splits.Length; i++)
+ {
+ if (!string.IsNullOrEmpty(splits[i]))
+ {
+ splits[length++] = splits[i];
+ }
+ }
+
+ Array.Clear(splits, length, splits.Length - length);
+ return splits.AsMemory(0, length);
+ }
+}
diff --git a/tests/Core/Components/Base/ComponentBaseTests.cs b/tests/Core/Components/Base/ComponentBaseTests.cs
index 91802c68e8..f74a5fe016 100644
--- a/tests/Core/Components/Base/ComponentBaseTests.cs
+++ b/tests/Core/Components/Base/ComponentBaseTests.cs
@@ -38,6 +38,7 @@ public class ComponentBaseTests : Bunit.TestContext
{ typeof(FluentRadioGroup<>), Loader.MakeGenericType(typeof(string)) },
//{ typeof(FluentRadio<>), Loader.MakeGenericType(typeof(string)) },
{ typeof(FluentTooltip), Loader.Default.WithRequiredParameter("Anchor", "MyButton").WithRequiredParameter("UseTooltipService", false)},
+ { typeof(FluentHighlighter), Loader.Default.WithRequiredParameter("HighlightedText", "AB").WithRequiredParameter("Text", "ABCDEF")},
};
///
diff --git a/tests/Core/Components/Highlighter/FluentHighlighterTests.razor b/tests/Core/Components/Highlighter/FluentHighlighterTests.razor
new file mode 100644
index 0000000000..a5eb6e1690
--- /dev/null
+++ b/tests/Core/Components/Highlighter/FluentHighlighterTests.razor
@@ -0,0 +1,103 @@
+@using Microsoft.FluentUI.AspNetCore.Components.Utilities
+@using Xunit;
+@using Microsoft.FluentUI.AspNetCore.Components.Tests.Samples;
+@inherits Bunit.TestContext
+
+@code
+{
+ public FluentHighlighterTests()
+ {
+ JSInterop.Mode = JSRuntimeMode.Loose;
+ Services.AddFluentUIComponents();
+ }
+
+ [Fact]
+ public void FluentHighlighter_Default()
+ {
+ // Arrange
+ var cut = Render(@);
+
+ // Act
+ var highlightedText = cut.FindAll("mark");
+
+ // Assert
+ Assert.Equal(3, highlightedText.Count);
+ Assert.Equal("ipsum", highlightedText[0].InnerHtml);
+ }
+
+ [Fact]
+ public void FluentHighlighter_CaseSensitive()
+ {
+ // Arrange
+ var cut = Render(@);
+
+ // Act
+ var highlightedText = cut.FindAll("mark");
+
+ // Assert
+ Assert.Single(highlightedText);
+ Assert.Equal("Ipsum", highlightedText[0].InnerHtml);
+ }
+
+ [Fact]
+ public void FluentHighlighter_WithDelimiters()
+ {
+ // Arrange
+ var cut = Render(@);
+
+ // Act
+ var highlightedText = cut.FindAll("mark");
+
+ // Assert
+ Assert.Equal(4, highlightedText.Count);
+ Assert.Equal("Lorem", highlightedText[0].InnerHtml);
+ Assert.Equal("ipsum", highlightedText[1].InnerHtml);
+ Assert.Equal("Lorem", highlightedText[2].InnerHtml);
+ Assert.Equal("ipsum", highlightedText[3].InnerHtml);
+ }
+
+ [Fact]
+ public void FluentHighlighter_UntilNextBoundary()
+ {
+ // Arrange
+ var cut = Render(@);
+
+ // Act
+ var highlightedText = cut.FindAll("mark");
+
+ // Assert
+ Assert.Equal(4, highlightedText.Count);
+ Assert.Equal("Lorem", highlightedText[0].InnerHtml);
+ Assert.Equal(" ipsum", highlightedText[1].InnerHtml);
+ Assert.Equal("Lorem", highlightedText[2].InnerHtml);
+ Assert.Equal(" ipsum", highlightedText[3].InnerHtml);
+ }
+
+ [Fact]
+ public void FluentHighlighter_HighlightedText_Empty()
+ {
+ // Arrange
+ var cut = Render(@);
+
+ // Act
+ var highlightedText = cut.FindAll("mark");
+
+ // Assert
+ Assert.Empty(highlightedText);
+ Assert.Equal("Lorem ipsum dolor sit amity.", cut.Markup);
+ }
+
+ [Fact]
+ public void FluentHighlighter_Text_Empty()
+ {
+ // Arrange
+ var cut = Render(@);
+
+ // Act
+ var highlightedText = cut.FindAll("mark");
+
+ // Assert
+ Assert.Empty(highlightedText);
+ Assert.Empty(cut.Markup);
+ }
+}