diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java
index 3ff6caf20..9bcaf73c3 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java
@@ -31,6 +31,7 @@
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
+import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.client.REIRuntime;
 import me.shedaniel.rei.api.client.gui.widgets.Widget;
 import me.shedaniel.rei.api.client.gui.widgets.Widgets;
@@ -43,16 +44,22 @@
 import me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl;
 import me.shedaniel.rei.impl.client.gui.config.components.ConfigCategoriesListWidget;
 import me.shedaniel.rei.impl.client.gui.config.components.ConfigEntriesListWidget;
+import me.shedaniel.rei.impl.client.gui.config.components.ConfigSearchListWidget;
 import me.shedaniel.rei.impl.client.gui.config.options.AllREIConfigCategories;
 import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption;
 import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory;
 import me.shedaniel.rei.impl.client.gui.config.options.OptionGroup;
 import me.shedaniel.rei.impl.client.gui.modules.Menu;
+import me.shedaniel.rei.impl.client.gui.widget.HoleWidget;
+import me.shedaniel.rei.impl.client.gui.widget.basewidgets.TextFieldWidget;
 import net.minecraft.ChatFormatting;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.gui.components.events.GuiEventListener;
 import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.client.resources.language.I18n;
+import net.minecraft.network.chat.TextComponent;
 import net.minecraft.network.chat.TranslatableComponent;
+import org.apache.commons.compress.harmony.unpack200.bytecode.forms.WideForm;
 import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.commons.lang3.mutable.MutableObject;
 import org.jetbrains.annotations.Nullable;
@@ -63,15 +70,17 @@
 import java.util.Map;
 import java.util.function.BiConsumer;
 
+import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.literal;
 import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.translatable;
 
 public class REIConfigScreen extends Screen implements ConfigAccess {
     private final Screen parent;
     private final List<OptionCategory> categories;
     private final List<Widget> widgets = new ArrayList<>();
-    private final Map<CompositeOption<?>, ?> defaultOptions = new HashMap<>();
-    private final Map<CompositeOption<?>, ?> options = new HashMap<>();
+    private final Map<String, ?> defaultOptions = new HashMap<>();
+    private final Map<String, ?> options = new HashMap<>();
     private OptionCategory activeCategory;
+    private boolean searching;
     @Nullable
     private Menu menu;
     @Nullable
@@ -95,8 +104,8 @@ public REIConfigScreen(Screen parent, List<OptionCategory> categories) {
         for (OptionCategory category : this.categories) {
             for (OptionGroup group : category.getGroups()) {
                 for (CompositeOption<?> option : group.getOptions()) {
-                    ((Map<CompositeOption<?>, Object>) this.defaultOptions).put(option, option.getBind().apply(defaultConfig));
-                    ((Map<CompositeOption<?>, Object>) this.options).put(option, option.getBind().apply(config));
+                    ((Map<String, Object>) this.defaultOptions).put(option.getId(), option.getBind().apply(defaultConfig));
+                    ((Map<String, Object>) this.options).put(option.getId(), option.getBind().apply(config));
                 }
             }
         }
@@ -111,7 +120,7 @@ private void cleanRequiresLevel() {
             for (OptionGroup group : category.getGroups()) {
                 group.getOptions().replaceAll(option -> {
                     if (option.isRequiresLevel()) {
-                        return new CompositeOption<>(option.getName(), option.getDescription(), i -> 0, (i, v) -> new Object())
+                        return new CompositeOption<>(option.getId(), option.getName(), option.getDescription(), i -> 0, (i, v) -> new Object())
                                 .entry(value -> translatable("config.rei.texts.requires_level").withStyle(ChatFormatting.RED))
                                 .defaultValue(() -> 1);
                     } else {
@@ -128,27 +137,53 @@ public void init() {
         this.widgets.clear();
         this.widgets.add(Widgets.createLabel(new Point(width / 2, 12), this.title));
         int sideWidth = (int) Math.round(width / 4.2);
-        boolean singlePane = width - 20 - sideWidth <= 330;
-        int singleSideWidth = 32 + 6 + 4;
-        Mutable<Widget> list = new MutableObject<>(createEntriesList(singlePane, singleSideWidth, sideWidth));
-        IntValue selectedCategory = new IntValue() {
-            @Override
-            public void accept(int index) {
-                REIConfigScreen.this.activeCategory = categories.get(index);
-                list.setValue(createEntriesList(singlePane, singleSideWidth, sideWidth));
-            }
-            
-            @Override
-            public int getAsInt() {
-                return categories.indexOf(activeCategory);
-            }
-        };
-        if (!singlePane) {
-            this.widgets.add(ConfigCategoriesListWidget.create(new Rectangle(8, 32, sideWidth, height - 32 - 32), categories, selectedCategory));
+        if (this.searching) {
+            this.widgets.add(Widgets.createButton(new Rectangle(8, 32, sideWidth, 20), literal("↩ ").append(translatable("gui.back")))
+                    .onClick(button -> setSearching(false)));
+            this.widgets.add(HoleWidget.createBackground(new Rectangle(8 + sideWidth + 4, 32, width - 16 - sideWidth - 4, 20), () -> 0, 32));
+            TextFieldWidget textField = new TextFieldWidget(new Rectangle(8 + sideWidth + 4 + 6, 32 + 6, width - 16 - sideWidth - 4 - 10, 12)) {
+                @Override
+                protected void renderSuggestion(PoseStack matrices, int x, int y) {
+                    int color;
+                    if (containsMouse(PointHelper.ofMouse()) || isFocused()) {
+                        color = 0xddeaeaea;
+                    } else {
+                        color = -6250336;
+                    }
+                    this.font.drawShadow(matrices, this.font.plainSubstrByWidth(this.getSuggestion(), this.getWidth()), x, y, color);
+                }
+            };
+            textField.setHasBorder(false);
+            textField.setMaxLength(9000);
+            this.widgets.add(textField);
+            this.widgets.add(Widgets.createDrawableWidget((helper, matrices, mouseX, mouseY, delta) -> {
+                textField.setSuggestion(!textField.isFocused() && textField.getText().isEmpty() ? I18n.get("config.rei.texts.search_options") : null);
+            }));
+            this.widgets.add(ConfigSearchListWidget.create(this, this.categories, textField, new Rectangle(8, 32 + 20 + 4, width - 16, height - 32 - (32 + 20 + 4))));
         } else {
-            this.widgets.add(ConfigCategoriesListWidget.createTiny(new Rectangle(8, 32, singleSideWidth - 4, height - 32 - 32), categories, selectedCategory));
+            boolean singlePane = width - 20 - sideWidth <= 330;
+            int singleSideWidth = 32 + 6 + 4;
+            Mutable<Widget> list = new MutableObject<>(createEntriesList(singlePane, singleSideWidth, sideWidth));
+            IntValue selectedCategory = new IntValue() {
+                @Override
+                public void accept(int index) {
+                    REIConfigScreen.this.activeCategory = categories.get(index);
+                    list.setValue(createEntriesList(singlePane, singleSideWidth, sideWidth));
+                }
+                
+                @Override
+                public int getAsInt() {
+                    return categories.indexOf(activeCategory);
+                }
+            };
+            if (!singlePane) {
+                this.widgets.add(ConfigCategoriesListWidget.create(new Rectangle(8, 32, sideWidth, height - 32 - 32), categories, selectedCategory));
+            } else {
+                this.widgets.add(ConfigCategoriesListWidget.createTiny(new Rectangle(8, 32, singleSideWidth - 4, height - 32 - 32), categories, selectedCategory));
+            }
+            this.widgets.add(Widgets.delegate(list::getValue));
         }
-        this.widgets.add(Widgets.delegate(list::getValue));
+        
         this.widgets.add(Widgets.createButton(new Rectangle(width / 2 - 150 - 10, height - 26, 150, 20), translatable("gui.cancel")).onClick(button -> {
             Minecraft.getInstance().setScreen(this.parent);
         }));
@@ -175,11 +210,11 @@ private Widget createEntriesList(boolean singlePane, int singleSideWidth, int si
         return ConfigEntriesListWidget.create(this, new Rectangle(singlePane ? 8 + singleSideWidth : 12 + sideWidth, 32, singlePane ? width - 16 - singleSideWidth : width - 20 - sideWidth, height - 32 - 32), activeCategory.getGroups());
     }
     
-    public Map<CompositeOption<?>, ?> getDefaultOptions() {
+    public Map<String, ?> getDefaultOptions() {
         return defaultOptions;
     }
     
-    public Map<CompositeOption<?>, ?> getOptions() {
+    public Map<String, ?> getOptions() {
         return options;
     }
     
@@ -367,17 +402,17 @@ public void closeMenu() {
     
     @Override
     public <T> T get(CompositeOption<T> option) {
-        return (T) getOptions().get(option);
+        return (T) getOptions().get(option.getId());
     }
     
     @Override
     public <T> void set(CompositeOption<T> option, T value) {
-        ((Map<CompositeOption<?>, Object>) getOptions()).put(option, value);
+        ((Map<String, Object>) getOptions()).put(option.getId(), value);
     }
     
     @Override
     public <T> T getDefault(CompositeOption<T> option) {
-        return (T) getDefaultOptions().get(option);
+        return (T) getDefaultOptions().get(option.getId());
     }
     
     @Override
@@ -397,4 +432,13 @@ public void focusKeycode(CompositeOption<ModifierKeyCode> option) {
     public CompositeOption<ModifierKeyCode> getFocusedKeycode() {
         return this.focusedKeycodeOption;
     }
+    
+    public void setSearching(boolean searching) {
+        this.searching = searching;
+        this.init(this.minecraft, this.width, this.height);
+    }
+    
+    public boolean isSearching() {
+        return searching;
+    }
 }
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java
index 3ab77951c..072c73a8d 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java
@@ -27,30 +27,56 @@
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.client.gui.widgets.Widget;
 import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds;
+import me.shedaniel.rei.api.common.util.CollectionUtils;
 import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory;
 import me.shedaniel.rei.impl.client.gui.widget.ListWidget;
 import me.shedaniel.rei.impl.client.gui.widget.ScrollableViewWidget;
 import me.shedaniel.rei.impl.common.util.RectangleUtils;
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableObject;
 
 import java.util.List;
 
 public class ConfigCategoriesListWidget {
     public static Widget create(Rectangle bounds, List<OptionCategory> categories, IntValue selected) {
-        WidgetWithBounds list = ListWidget.builderOf(RectangleUtils.inset(bounds, 3, 5), categories,
-                        (index, entry) -> ConfigCategoryEntryWidget.create(entry, bounds.width - 6))
+        final Mutable<WidgetWithBounds> list = new MutableObject<>(null);
+        list.setValue(ListWidget.builderOfWidgets(RectangleUtils.inset(bounds, 3, 5),
+                        CollectionUtils.concatUnmodifiable(List.of(ConfigSearchWidget.create(() -> list.getValue() != null && list.getValue().getBounds().height + 6 > bounds.height ? bounds.width - 6 - 6 : bounds.width - 6)),
+                                CollectionUtils.map(categories, entry -> ConfigCategoryEntryWidget.create(entry, bounds.width - 6))))
                 .gap(3)
-                .isSelectable((index, entry) -> true)
-                .selected(selected)
-                .build();
-        return ScrollableViewWidget.create(bounds, list.withPadding(0, 5), true);
+                .isSelectable((index, entry) -> index != 0)
+                .selected(new IntValue() {
+                    @Override
+                    public void accept(int value) {
+                        selected.accept(value - 1);
+                    }
+                    
+                    @Override
+                    public int getAsInt() {
+                        return selected.getAsInt() + 1;
+                    }
+                })
+                .build());
+        return ScrollableViewWidget.create(bounds, list.getValue().withPadding(0, 5), true);
     }
     
     public static Widget createTiny(Rectangle bounds, List<OptionCategory> categories, IntValue selected) {
-        WidgetWithBounds list = ListWidget.builderOf(RectangleUtils.inset(bounds, (bounds.width - 6 - 16) / 2, 9), categories,
-                        (index, entry) -> ConfigCategoryEntryWidget.createTiny(entry))
+        WidgetWithBounds list = ListWidget.builderOfWidgets(RectangleUtils.inset(bounds, (bounds.width - 6 - 16) / 2, 9),
+                        CollectionUtils.concatUnmodifiable(List.of(ConfigSearchWidget.createTiny()),
+                                CollectionUtils.map(categories, ConfigCategoryEntryWidget::createTiny)))
                 .gap(7)
-                .isSelectable((index, entry) -> true)
-                .selected(selected)
+                .isSelectable((index, entry) -> index != 0)
+                .selected(new IntValue() {
+                    @Override
+                    public void accept(int value) {
+                        selected.accept(value - 1);
+                    }
+                    
+                    @Override
+                    public int getAsInt() {
+                        return selected.getAsInt() + 1;
+                    }
+                })
                 .build();
         return ScrollableViewWidget.create(bounds, list.withPadding(0, 9), true);
     }
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java
index b1ecca932..906148b75 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java
@@ -37,7 +37,7 @@
 public class ConfigEntriesListWidget {
     public static Widget create(ConfigAccess access, Rectangle bounds, List<OptionGroup> groups) {
         WidgetWithBounds list = ListWidget.builderOf(RectangleUtils.inset(bounds, 6, 6), groups,
-                        (index, entry) -> ConfigGroupWidget.create(access, entry, bounds.width - 12 - 6))
+                        (index, entry) -> ConfigGroupWidget.create(access, entry, bounds.width - 12 - 6, false))
                 .gap(7)
                 .calculateTotalHeightDynamically(true)
                 .build();
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java
index 063596f1a..e41c233b2 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java
@@ -37,6 +37,7 @@
 import me.shedaniel.rei.impl.client.gui.config.options.preview.AccessibilityDisplayPreviewer;
 import me.shedaniel.rei.impl.client.gui.config.options.preview.InterfacePreviewer;
 import me.shedaniel.rei.impl.client.gui.config.options.preview.TooltipPreviewer;
+import me.shedaniel.rei.impl.client.gui.text.TextTransformations;
 import net.minecraft.client.gui.GuiComponent;
 import org.apache.commons.lang3.tuple.Pair;
 import org.jetbrains.annotations.Nullable;
@@ -59,13 +60,13 @@ public static void addPreview(OptionGroup group, PreviewLocation location, Speci
         SPECIAL_GROUPS.put(group, Pair.of(location, constructor));
     }
     
-    public static WidgetWithBounds create(ConfigAccess access, OptionGroup entry, int width) {
-        WidgetWithBounds groupTitle = Widgets.createLabel(new Point(0, 3), entry.getGroupName().copy().withStyle(style -> style.withColor(0xFFC0C0C0).withUnderlined(true)))
+    public static WidgetWithBounds create(ConfigAccess access, OptionGroup entry, int width, boolean applyPreview) {
+        WidgetWithBounds groupTitle = Widgets.createLabel(new Point(0, 3), TextTransformations.highlightText(entry.getGroupName().copy(), entry.getGroupNameHighlight(), style -> style.withColor(0xFFC0C0C0).withUnderlined(true)))
                 .leftAligned()
                 .withPadding(0, 0, 0, 6);
         WidgetWithBounds contents;
         
-        if (SPECIAL_GROUPS.containsKey(entry)) {
+        if (applyPreview && SPECIAL_GROUPS.containsKey(entry)) {
             Pair<PreviewLocation, SpecialGroupConstructor> pair = SPECIAL_GROUPS.get(entry);
             PreviewLocation location = pair.getLeft();
             int halfWidth = width * 6 / 10 - 2;
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java
index a449700a0..f2a47a332 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java
@@ -38,6 +38,7 @@
 import me.shedaniel.rei.impl.client.gui.config.ConfigAccess;
 import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption;
 import me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils;
+import me.shedaniel.rei.impl.client.gui.text.TextTransformations;
 import net.minecraft.Util;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.gui.components.events.GuiEventListener;
@@ -57,17 +58,17 @@ public static <T> WidgetWithBounds create(ConfigAccess access, CompositeOption<T
         int[] stableHeight = {12};
         int[] height = {12};
         Label fieldNameLabel;
-        widgets.add(fieldNameLabel = Widgets.createLabel(new Point(0, 0), option.getName().copy().withStyle(style -> style.withColor(0xFFC0C0C0)))
+        widgets.add(fieldNameLabel = Widgets.createLabel(new Point(0, 0), TextTransformations.highlightText(option.getName().copy(), option.getOptionNameHighlight(), style -> style.withColor(0xFFC0C0C0)))
                 .leftAligned());
         WidgetWithBounds optionValue = ConfigOptionValueWidget.create(access, option, width - 10 - fieldNameLabel.getBounds().width);
         widgets.add(Widgets.withTranslate(optionValue, () -> Matrix4f.createTranslateMatrix(width - optionValue.getBounds().width - optionValue.getBounds().x, 0, 0)));
         widgets.add(new WidgetWithBounds() {
             final MutableComponent description = Util.make(() -> {
-                MutableComponent description = option.getDescription().copy().withStyle(style -> style.withColor(0xFF757575));
+                MutableComponent description = option.getDescription().copy();
                 if (description.getString().endsWith(".desc")) {
                     return literal("");
                 } else {
-                    return description;
+                    return TextTransformations.highlightText(description, option.getOptionDescriptionHighlight(), style -> style.withColor(0xFF757575));
                 }
             });
             
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchListWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchListWidget.java
new file mode 100644
index 000000000..034203b88
--- /dev/null
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchListWidget.java
@@ -0,0 +1,127 @@
+package me.shedaniel.rei.impl.client.gui.config.components;
+
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.client.gui.widgets.TextField;
+import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds;
+import me.shedaniel.rei.api.client.gui.widgets.Widgets;
+import me.shedaniel.rei.impl.client.gui.config.ConfigAccess;
+import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption;
+import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory;
+import me.shedaniel.rei.impl.client.gui.config.options.OptionGroup;
+import me.shedaniel.rei.impl.client.gui.widget.ListWidget;
+import me.shedaniel.rei.impl.client.gui.widget.ScrollableViewWidget;
+import me.shedaniel.rei.impl.client.gui.widget.basewidgets.TextFieldWidget;
+import me.shedaniel.rei.impl.common.util.RectangleUtils;
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableObject;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class ConfigSearchListWidget {
+    public static WidgetWithBounds create(ConfigAccess access, List<OptionCategory> categories, TextField textField, Rectangle bounds) {
+        Mutable<WidgetWithBounds> list = new MutableObject<>(null);
+        Consumer<String> responder = string -> {
+            Collection<ConfigSearchWidget.SearchResult> results = ConfigSearchWidget.matchResult(categories, string);
+            list.setValue(createList(access, results, string, bounds));
+        };
+        ((TextFieldWidget) textField).setResponder(responder);
+        responder.accept(textField.getText());
+        
+        return ScrollableViewWidget.create(bounds, Widgets.delegateWithBounds(list::getValue), true);
+    }
+    
+    private static WidgetWithBounds createList(ConfigAccess access, Collection<ConfigSearchWidget.SearchResult> results, String searchTerm, Rectangle bounds) {
+        return ListWidget.builderOfWidgets(RectangleUtils.inset(bounds, 6, 6),
+                        collectResultWidgets(access, results, searchTerm, bounds))
+                .gap(7)
+                .calculateTotalHeightDynamically(true)
+                .build()
+                .withPadding(0, 5);
+    }
+    
+    private static List<WidgetWithBounds> collectResultWidgets(ConfigAccess access, Collection<ConfigSearchWidget.SearchResult> results, String searchTerm, Rectangle bounds) {
+        List<ConfigSearchWidget.SearchResult> collapsedResults = new ArrayList<>();
+        for (ConfigSearchWidget.SearchResult result : results) {
+            if (result instanceof ConfigSearchWidget.IndividualResult individualResult) {
+                int lastMatchGroup = -1;
+                for (int i = 0; i < collapsedResults.size(); i++) {
+                    ConfigSearchWidget.SearchResult prev = collapsedResults.get(i);
+                    if (prev instanceof ConfigSearchWidget.IndividualResult prevInd && prevInd.group().getGroupName().getString().equals(individualResult.group().getGroupName().getString())) {
+                        lastMatchGroup = i;
+                    }
+                }
+                if (lastMatchGroup == -1) {
+                    collapsedResults.add(result);
+                } else {
+                    collapsedResults.add(lastMatchGroup + 1, result);
+                }
+            } else {
+                collapsedResults.add(result);
+            }
+        }
+        
+        List<WidgetWithBounds> widgets = new ArrayList<>();
+        ConfigSearchWidget.SearchResult last = null;
+        List<CompositeOption<?>> merge = null;
+        for (ConfigSearchWidget.SearchResult result : collapsedResults) {
+            if (last instanceof ConfigSearchWidget.IndividualResult lastInd && result instanceof ConfigSearchWidget.IndividualResult currInd) {
+                if (lastInd.group().getGroupName().getString().equals(currInd.group().getGroupName().getString())) {
+                    if (merge != null) {
+                        merge.add(((ConfigSearchWidget.IndividualResult) currInd.decompose(searchTerm)).option());
+                    } else {
+                        merge = new ArrayList<>(List.of(((ConfigSearchWidget.IndividualResult) lastInd.decompose(searchTerm)).option(),
+                                ((ConfigSearchWidget.IndividualResult) currInd.decompose(searchTerm)).option()));
+                    }
+                    
+                    last = result;
+                    continue;
+                }
+            }
+            
+            if (last != null) {
+                // Commit last
+                if (merge != null) {
+                    OptionGroup group = ((ConfigSearchWidget.IndividualResult) last).group().copy();
+                    group.getOptions().clear();
+                    group.getOptions().addAll(merge);
+                    merge = null;
+                    widgets.add(createSearchResult(access, group, bounds.width - 12 - 6));
+                } else {
+                    widgets.add(createSearchResult(access, last.decompose(searchTerm), bounds.width - 12 - 6));
+                }
+            }
+            last = result;
+        }
+        
+        if (last != null) {
+            // Commit last
+            if (merge != null) {
+                OptionGroup group = ((ConfigSearchWidget.IndividualResult) last).group().copy();
+                group.getOptions().clear();
+                group.getOptions().addAll(merge);
+                widgets.add(createSearchResult(access, group, bounds.width - 12 - 6));
+            } else {
+                widgets.add(createSearchResult(access, last.decompose(searchTerm), bounds.width - 12 - 6));
+            }
+        }
+        
+        return widgets;
+    }
+    
+    private static WidgetWithBounds createSearchResult(ConfigAccess access, Object result, int width) {
+        if (result instanceof OptionCategory category) return Widgets.noOp();
+        if (result instanceof OptionGroup group) {
+            return ConfigGroupWidget.create(access, group, width, false);
+        }
+        if (result instanceof ConfigSearchWidget.IndividualResult individualResult) {
+            OptionGroup group = individualResult.group().copy();
+            group.getOptions().clear();
+            group.getOptions().add(individualResult.option());
+            return ConfigGroupWidget.create(access, group, width, false);
+        }
+        return Widgets.noOp();
+    }
+}
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchWidget.java
new file mode 100644
index 000000000..e0012a575
--- /dev/null
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchWidget.java
@@ -0,0 +1,373 @@
+package me.shedaniel.rei.impl.client.gui.config.components;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.client.gui.widgets.Label;
+import me.shedaniel.rei.api.client.gui.widgets.Widget;
+import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds;
+import me.shedaniel.rei.api.client.gui.widgets.Widgets;
+import me.shedaniel.rei.impl.client.gui.config.REIConfigScreen;
+import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption;
+import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory;
+import me.shedaniel.rei.impl.client.gui.config.options.OptionGroup;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.Font;
+import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.*;
+import java.util.function.IntSupplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.translatable;
+
+public class ConfigSearchWidget {
+    public static WidgetWithBounds create(IntSupplier width) {
+        Label label = Widgets.createLabel(new Point(21, 6), translatable("config.rei.texts.search_options"))
+                .leftAligned();
+        Font font = Minecraft.getInstance().font;
+        Rectangle bounds = new Rectangle(0, 0, label.getBounds().getMaxX(), 7 * 3);
+        return Widgets.concatWithBounds(
+                bounds,
+                new Widget() {
+                    @Override
+                    public void render(PoseStack poses, int mouseX, int mouseY, float delta) {
+                        boolean hovering = new Rectangle(-1, -1, width.getAsInt() + 2, 21).contains(mouseX, mouseY);
+                        for (Widget widget : List.of(Widgets.createFilledRectangle(new Rectangle(1, 1, width.getAsInt() - 2, 18), hovering ? 0x50FFFFFF : 0x25FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(-1, -1, width.getAsInt() + 2, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(-1, 20, width.getAsInt() + 2, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(-1, 0, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(width.getAsInt(), 0, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF))) {
+                            widget.render(poses, mouseX, mouseY, delta);
+                        }
+                        label.setColor(hovering ? 0xFFE1E1E1 : 0xFFC0C0C0);
+                    }
+                    
+                    @Override
+                    public List<? extends GuiEventListener> children() {
+                        return List.of();
+                    }
+                    
+                    @Override
+                    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+                        if (new Rectangle(-1, -1, width.getAsInt() + 2, 21).contains(mouseX, mouseY)) {
+                            Widgets.produceClickSound();
+                            ((REIConfigScreen) Minecraft.getInstance().screen).setSearching(true);
+                            return true;
+                        }
+                        
+                        return false;
+                    }
+                },
+                Widgets.withTranslate(label, 0, 0.5, 0),
+                Widgets.createTexturedWidget(new ResourceLocation("roughlyenoughitems:textures/gui/config/search_options.png"), new Rectangle(3, 3, 16, 16), 0, 0, 1, 1, 1, 1)
+        
+        );
+    }
+    
+    public static WidgetWithBounds createTiny() {
+        Rectangle bounds = new Rectangle(0, 0, 16, 16);
+        return Widgets.withTooltip(Widgets.concatWithBounds(
+                bounds,
+                new Widget() {
+                    @Override
+                    public void render(PoseStack poses, int mouseX, int mouseY, float delta) {
+                        boolean hovering = new Rectangle(-1, -1, 18, 18).contains(mouseX, mouseY);
+                        poses.pushPose();
+                        poses.translate(-0.5, -0.5, 0);
+                        for (Widget widget : List.of(Widgets.createFilledRectangle(new Rectangle(-1, -1, 18, 18), hovering ? 0x50FFFFFF : 0x25FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(-3, -3, 22, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(-3, 18, 22, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(-3, -2, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF),
+                                Widgets.createFilledRectangle(new Rectangle(18, -2, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF))) {
+                            widget.render(poses, mouseX, mouseY, delta);
+                        }
+                        poses.popPose();
+                    }
+                    
+                    @Override
+                    public List<? extends GuiEventListener> children() {
+                        return List.of();
+                    }
+                    
+                    @Override
+                    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+                        if (new Rectangle(-1, -1, 18, 18).contains(mouseX, mouseY)) {
+                            Widgets.produceClickSound();
+                            ((REIConfigScreen) Minecraft.getInstance().screen).setSearching(true);
+                            return true;
+                        }
+                        
+                        return false;
+                    }
+                },
+                Widgets.createTexturedWidget(new ResourceLocation("roughlyenoughitems:textures/gui/config/search_options.png"), bounds, 0, 0, 1, 1, 1, 1)
+        
+        ), translatable("config.rei.texts.search_options"));
+    }
+    
+    public static Collection<SearchResult> matchResult(List<OptionCategory> categories, String searchTerm) {
+        if (searchTerm.isBlank()) return Collections.emptyList();
+        List<ScoredResult> scoredResults = collectSearchResults(categories, searchTerm);
+        // Remove categories results for now, we might add it back later.
+        scoredResults.removeIf(result -> result.result() instanceof CategoryResult);
+        
+        // Distinct them.
+        Set<SearchResult> distinctResults = new LinkedHashSet<>();
+        for (ScoredResult result : scoredResults) {
+            if (result.score() >= 0.001F) {
+                distinctResults.add(result.result());
+            }
+        }
+        
+        // Remove duplicates.
+        Set<OptionCategory> visitedCategories = new HashSet<>();
+        Set<OptionGroup> visitedGroups = new HashSet<>();
+        Iterator<SearchResult> iterator = distinctResults.iterator();
+        while (iterator.hasNext()) {
+            SearchResult result = iterator.next();
+            if (result instanceof CategoryResult categoryResult) {
+                visitedCategories.add(categoryResult.category());
+            } else if (result instanceof GroupResult groupResult) {
+                if (visitedCategories.contains(((GroupResult) result).category)) {
+                    iterator.remove();
+                } else {
+                    visitedGroups.add(groupResult.group());
+                }
+            } else if (result instanceof IndividualResult individualResult) {
+                if (visitedCategories.contains(individualResult.category()) || visitedGroups.contains(individualResult.group())) {
+                    iterator.remove();
+                }
+            }
+        }
+        
+        return distinctResults;
+    }
+    
+    private static List<ScoredResult> collectSearchResults(List<OptionCategory> categories, String searchTerm) {
+        String lcSearchTerm = searchTerm.toLowerCase(Locale.ROOT);
+        
+        // Find all options that match.
+        List<SearchResult> results = new ArrayList<>();
+        List<SearchResult> fuzzyResults = new ArrayList<>();
+        for (OptionCategory category : categories) {
+            if (category.getKey().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                results.add(new CategoryResult(category, new MatchComposite(category.getKey(), MatchType.KEY)));
+            } else {
+                fuzzyResults.add(new CategoryResult(category, new MatchComposite(category.getKey(), MatchType.KEY)));
+            }
+            if (category.getName().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                results.add(new CategoryResult(category, new MatchComposite(category.getName().getString(), MatchType.NAME)));
+            } else {
+                fuzzyResults.add(new CategoryResult(category, new MatchComposite(category.getName().getString(), MatchType.NAME)));
+            }
+            if (!category.getDescription().getString().endsWith(".desc") && category.getDescription().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                results.add(new CategoryResult(category, new MatchComposite(category.getDescription().getString(), MatchType.DESCRIPTION)));
+            } else {
+                fuzzyResults.add(new CategoryResult(category, new MatchComposite(category.getDescription().getString(), MatchType.DESCRIPTION)));
+            }
+            
+            for (OptionGroup group : category.getGroups()) {
+                if (group.getId().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                    results.add(new GroupResult(category, group, new MatchComposite(group.getId(), MatchType.KEY)));
+                } else {
+                    fuzzyResults.add(new GroupResult(category, group, new MatchComposite(group.getId(), MatchType.KEY)));
+                }
+                if (group.getGroupName().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                    results.add(new GroupResult(category, group, new MatchComposite(group.getGroupName().getString(), MatchType.NAME)));
+                } else {
+                    fuzzyResults.add(new GroupResult(category, group, new MatchComposite(group.getGroupName().getString(), MatchType.NAME)));
+                }
+                
+                for (CompositeOption<?> option : group.getOptions()) {
+                    if (option.getId().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                        results.add(new IndividualResult(category, group, option, new MatchComposite(option.getId(), MatchType.KEY)));
+                    } else {
+                        fuzzyResults.add(new IndividualResult(category, group, option, new MatchComposite(option.getId(), MatchType.KEY)));
+                    }
+                    if (option.getName().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                        results.add(new IndividualResult(category, group, option, new MatchComposite(option.getName().getString(), MatchType.NAME)));
+                    } else {
+                        fuzzyResults.add(new IndividualResult(category, group, option, new MatchComposite(option.getName().getString(), MatchType.NAME)));
+                    }
+                    if (!option.getDescription().getString().endsWith(".desc") && option.getDescription().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) {
+                        results.add(new IndividualResult(category, group, option, new MatchComposite(option.getDescription().getString(), MatchType.DESCRIPTION)));
+                    } else {
+                        fuzzyResults.add(new IndividualResult(category, group, option, new MatchComposite(option.getDescription().getString(), MatchType.DESCRIPTION)));
+                    }
+                }
+            }
+        }
+        
+        return Stream.concat(results.stream().map(result -> {
+                    return new ScoredResult(result, similarity(result.matched().matched(), searchTerm) * result.matched().type().multiplier());
+                }), fuzzyResults.stream().map(result -> {
+                    return new ScoredResult(result, (float) Math.pow(similarity(result.matched().matched(), searchTerm), 1.5) * result.matched().type().multiplier() * 0.9F);
+                }).filter(result -> result.score() > 0.5F)).sorted(Comparator.comparingDouble(ScoredResult::score).reversed())
+                .collect(Collectors.toList());
+    }
+    
+    public interface SearchResult {
+        MatchComposite matched();
+        
+        Object decompose(String searchTerm);
+    }
+    
+    public record ScoredResult(
+            SearchResult result,
+            float score
+    ) {
+    }
+    
+    public record IndividualResult(
+            OptionCategory category,
+            OptionGroup group,
+            CompositeOption<?> option,
+            MatchComposite matched
+    ) implements SearchResult {
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            IndividualResult that = (IndividualResult) o;
+            return Objects.equals(option, that.option);
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hash(option);
+        }
+        
+        @Override
+        public Object decompose(String searchTerm) {
+            CompositeOption<?> copied = option().copy();
+            if (matched().type() == MatchType.NAME) copied.setOptionNameHighlight(searchTerm);
+            else if (matched().type() == MatchType.DESCRIPTION) copied.setOptionDescriptionHighlight(searchTerm);
+            return new IndividualResult(category(), group(), copied, matched());
+        }
+    }
+    
+    public record GroupResult(
+            OptionCategory category,
+            OptionGroup group,
+            MatchComposite matched
+    ) implements SearchResult {
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            GroupResult that = (GroupResult) o;
+            return Objects.equals(group, that.group);
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hash(group);
+        }
+        
+        @Override
+        public Object decompose(String searchTerm) {
+            OptionGroup copied = this.group().copy();
+            copied.setGroupNameHighlight(searchTerm);
+            return copied;
+        }
+    }
+    
+    public record CategoryResult(
+            OptionCategory category,
+            MatchComposite matched
+    ) implements SearchResult {
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            CategoryResult that = (CategoryResult) o;
+            return Objects.equals(category, that.category);
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hash(category);
+        }
+        
+        @Override
+        public Object decompose(String searchTerm) {
+            return this.category();
+        }
+    }
+    
+    public enum MatchType {
+        KEY(1.0F),
+        NAME(0.98F),
+        DESCRIPTION(0.8F),
+        ;
+        
+        private final float multiplier;
+        
+        MatchType(float multiplier) {
+            this.multiplier = multiplier;
+        }
+        
+        public float multiplier() {
+            return multiplier;
+        }
+    }
+    
+    public record MatchComposite(
+            String matched,
+            MatchType type
+    ) {
+    }
+    
+    private static float similarity(String first, String second) {
+        String firstLowerCase = first.toLowerCase(Locale.ROOT);
+        String secondLowerCase = second.toLowerCase(Locale.ROOT);
+        if (!Objects.equals(first, firstLowerCase) || !Objects.equals(second, secondLowerCase)) {
+            return (innerSimilarity(first, second) + innerSimilarity(firstLowerCase, secondLowerCase)) / 2.0F;
+        } else {
+            return innerSimilarity(first, second);
+        }
+    }
+    
+    private static float innerSimilarity(String first, String second) {
+        String longer = first;
+        String shorter = second;
+        if (first.length() < second.length()) {
+            longer = second;
+            shorter = first;
+        }
+        int longerLength = longer.length();
+        if (longerLength == 0) {
+            return 1.0F;
+        } else {
+            return (longerLength - editDistance(longer, shorter)) / ((float) longerLength);
+        }
+    }
+    
+    private static int editDistance(String s11, String s22) {
+        int[] costs = new int[s22.length() + 1];
+        for (int i = 0; i <= s11.length(); i++) {
+            int lastValue = i;
+            for (int j = 0; j <= s22.length(); j++) {
+                if (i == 0) {
+                    costs[j] = j;
+                } else {
+                    if (j > 0) {
+                        int newValue = costs[j - 1];
+                        if (s11.charAt(i - 1) != s22.charAt(j - 1)) {
+                            newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
+                        }
+                        costs[j - 1] = lastValue;
+                        lastValue = newValue;
+                    }
+                }
+            }
+            if (i > 0) {
+                costs[s22.length()] = lastValue;
+            }
+        }
+        return costs[s22.length()];
+    }
+}
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java
index 469013a0a..9122bf1d4 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java
@@ -33,7 +33,7 @@
 
 public interface AllREIConfigCategories {
     static OptionCategory make(String key) {
-        return OptionCategory.of(new ResourceLocation("roughlyenoughitems:textures/gui/config/" + key + ".png"),
+        return OptionCategory.of(key, new ResourceLocation("roughlyenoughitems:textures/gui/config/" + key + ".png"),
                 translatable("config.rei.categories." + key),
                 translatable("config.rei.categories." + key + ".desc"));
     }
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java
index 32e2a002d..9e392d8df 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java
@@ -28,7 +28,7 @@
 
 public interface AllREIConfigGroups {
     static <T> OptionGroup make(String id) {
-        return new OptionGroup(translatable("config.rei.options.groups." + id));
+        return new OptionGroup(id, translatable("config.rei.options.groups." + id));
     }
     
     OptionGroup APPEARANCE_INTERFACE = make("appearance.interface")
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java
index b534d40e4..f09cefdd4 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java
@@ -57,7 +57,7 @@
 public interface AllREIConfigOptions {
     static <T> CompositeOption<T> make(String id, Function<ConfigObjectImpl, T> bind,
                                        BiConsumer<ConfigObjectImpl, T> save) {
-        return new CompositeOption<>(translatable("config.rei.options." + id),
+        return new CompositeOption<>(id, translatable("config.rei.options." + id),
                 translatable("config.rei.options." + id + ".desc"), bind, save);
     }
     
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java
index fed950b48..e9f932759 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java
@@ -33,6 +33,7 @@
 import java.util.function.Supplier;
 
 public class CompositeOption<T> {
+    private final String id;
     private final Component name;
     private final Component description;
     private final Function<ConfigObjectImpl, T> bind;
@@ -43,8 +44,13 @@ public class CompositeOption<T> {
     private Supplier<T> defaultValue = null;
     private OptionValueEntry<T> entry = OptionValueEntry.noOp();
     private boolean requiresLevel = false;
+    @Nullable
+    private String optionNameHighlight = null;
+    @Nullable
+    private String optionDescriptionHighlight = null;
     
-    public CompositeOption(Component name, Component description, Function<ConfigObjectImpl, T> bind, BiConsumer<ConfigObjectImpl, T> save) {
+    public CompositeOption(String id, Component name, Component description, Function<ConfigObjectImpl, T> bind, BiConsumer<ConfigObjectImpl, T> save) {
+        this.id = id;
         this.name = name;
         this.description = description;
         this.bind = bind;
@@ -101,10 +107,28 @@ public CompositeOption<T> requiresLevel() {
         return this;
     }
     
+    public void setOptionNameHighlight(@Nullable String optionNameHighlight) {
+        this.optionNameHighlight = optionNameHighlight;
+    }
+    
+    public void setOptionDescriptionHighlight(@Nullable String optionDescriptionHighlight) {
+        this.optionDescriptionHighlight = optionDescriptionHighlight;
+    }
+    
     public boolean isRequiresLevel() {
         return requiresLevel;
     }
     
+    @Nullable
+    public String getOptionNameHighlight() {
+        return optionNameHighlight;
+    }
+    
+    @Nullable
+    public String getOptionDescriptionHighlight() {
+        return optionDescriptionHighlight;
+    }
+    
     public CompositeOption<T> previewer(ConfigPreviewer<T> previewer) {
         this.previewer = previewer;
         return this;
@@ -115,6 +139,10 @@ public CompositeOption<T> defaultValue(Supplier<T> defaultValue) {
         return this;
     }
     
+    public String getId() {
+        return id;
+    }
+    
     public Component getName() {
         return name;
     }
@@ -151,7 +179,7 @@ public T getDefaultValue() {
     }
     
     public CompositeOption<T> copy() {
-        CompositeOption<T> option = new CompositeOption<>(name, description, bind, save);
+        CompositeOption<T> option = new CompositeOption<>(id, name, description, bind, save);
         option.entry = entry;
         option.previewer = previewer;
         option.defaultValue = defaultValue;
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java
index 5a9eaaf6a..536b39892 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java
@@ -30,19 +30,21 @@
 import java.util.List;
 
 public class OptionCategory {
+    private final String key;
     private final ResourceLocation icon;
     private final Component name;
     private final Component description;
     private final List<OptionGroup> groups = new ArrayList<>();
     
-    private OptionCategory(ResourceLocation icon, Component name, Component description) {
+    private OptionCategory(String key, ResourceLocation icon, Component name, Component description) {
+        this.key = key;
         this.icon = icon;
         this.name = name;
         this.description = description;
     }
     
-    public static OptionCategory of(ResourceLocation icon, Component name, Component description) {
-        return new OptionCategory(icon, name, description);
+    public static OptionCategory of(String key, ResourceLocation icon, Component name, Component description) {
+        return new OptionCategory(key, icon, name, description);
     }
     
     public OptionCategory add(OptionGroup group) {
@@ -50,6 +52,10 @@ public OptionCategory add(OptionGroup group) {
         return this;
     }
     
+    public String getKey() {
+        return key;
+    }
+    
     public ResourceLocation getIcon() {
         return icon;
     }
@@ -67,7 +73,7 @@ public List<OptionGroup> getGroups() {
     }
     
     public OptionCategory copy() {
-        OptionCategory category = new OptionCategory(icon, name, description);
+        OptionCategory category = new OptionCategory(key, icon, name, description);
         for (OptionGroup group : groups) {
             category.add(group.copy());
         }
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java
index 4123712b4..8830bef33 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java
@@ -24,16 +24,21 @@
 package me.shedaniel.rei.impl.client.gui.config.options;
 
 import net.minecraft.network.chat.Component;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
 public class OptionGroup {
+    private final String id;
     private final Component groupName;
     private final List<CompositeOption<?>> options = new ArrayList<>();
+    @Nullable
+    private String groupNameHighlight = null;
     
-    public OptionGroup(Component groupName) {
+    public OptionGroup(String id, Component groupName) {
+        this.id = id;
         this.groupName = groupName;
     }
     
@@ -42,6 +47,14 @@ public OptionGroup add(CompositeOption<?> option) {
         return this;
     }
     
+    public void setGroupNameHighlight(@Nullable String groupNameHighlight) {
+        this.groupNameHighlight = groupNameHighlight;
+    }
+    
+    public String getId() {
+        return id;
+    }
+    
     public Component getGroupName() {
         return groupName;
     }
@@ -50,6 +63,11 @@ public List<CompositeOption<?>> getOptions() {
         return options;
     }
     
+    @Nullable
+    public String getGroupNameHighlight() {
+        return groupNameHighlight;
+    }
+    
     @Override
     public boolean equals(Object obj) {
         if (obj instanceof OptionGroup group) {
@@ -65,7 +83,7 @@ public int hashCode() {
     }
     
     public OptionGroup copy() {
-        OptionGroup group = new OptionGroup(groupName);
+        OptionGroup group = new OptionGroup(id, groupName);
         for (CompositeOption<?> option : options) {
             group.add(option);
         }
diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java
index 1a15d4583..d7a8d0a1c 100644
--- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java
+++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java
@@ -24,12 +24,20 @@
 package me.shedaniel.rei.impl.client.gui.text;
 
 import me.shedaniel.math.Color;
+import net.minecraft.ChatFormatting;
 import net.minecraft.Util;
 import net.minecraft.client.Minecraft;
+import net.minecraft.network.chat.MutableComponent;
 import net.minecraft.network.chat.Style;
 import net.minecraft.network.chat.TextColor;
 import net.minecraft.util.FormattedCharSequence;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Locale;
+import java.util.function.UnaryOperator;
+
+import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.literal;
 
 @ApiStatus.Internal
 public class TextTransformations {
@@ -92,6 +100,35 @@ public static FormattedCharSequence forwardWithTransformation(String text, CharS
         };
     }
     
+    public static MutableComponent highlightText(MutableComponent component, @Nullable String highlight, UnaryOperator<Style> styleOperator) {
+        if (highlight == null) return component.withStyle(styleOperator);
+        String string = component.getString();
+        if (string.toLowerCase(Locale.ROOT).equals(highlight.toLowerCase(Locale.ROOT))) {
+            return component.withStyle(styleOperator).withStyle(ChatFormatting.YELLOW);
+        }
+        String[] parts = string.toLowerCase(Locale.ROOT).split(highlight.toLowerCase(Locale.ROOT));
+        if (string.toLowerCase(Locale.ROOT).endsWith(highlight.toLowerCase(Locale.ROOT))) {
+            // Append an empty string to the end
+            String[] newParts = new String[parts.length + 1];
+            System.arraycopy(parts, 0, newParts, 0, parts.length);
+            newParts[parts.length] = "";
+            parts = newParts;
+        }
+        if (parts.length <= 1) return component.withStyle(styleOperator);
+        MutableComponent output = literal("");
+        int curr = 0;
+        for (int i = 0; i < parts.length; i++) {
+            output.append(literal(string.substring(curr, curr + parts[i].length())).withStyle(styleOperator));
+            curr += parts[i].length();
+            if (i != parts.length - 1) {
+                output.append(literal(string.substring(curr, curr + highlight.length())).withStyle(styleOperator)
+                        .withStyle(ChatFormatting.YELLOW));
+                curr += highlight.length();
+            }
+        }
+        return output;
+    }
+    
     @FunctionalInterface
     public interface CharSequenceTransformer {
         Style apply(String text, int charIndex, char c);
diff --git a/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json b/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json
index 4937b183d..abb922c40 100755
--- a/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json
+++ b/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json
@@ -462,6 +462,7 @@
   "config.rei.value.trueFalse.true": "True",
   "config.rei.value.enabledDisabled.false": "Disabled",
   "config.rei.value.enabledDisabled.true": "Enabled",
+  "config.rei.texts.search_options": "Search options...",
   "config.rei.texts.preview": "Preview...",
   "config.rei.texts.configure": "Configure...",
   "config.rei.texts.details": "Details...",
diff --git a/runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/config/search_options.png b/runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/config/search_options.png
new file mode 100644
index 000000000..b7b3696ee
Binary files /dev/null and b/runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/config/search_options.png differ