Repository: gradio-app/gradio
Pull request: Dropdown Component Updates
# Description

Default dropdown uses the custom dropdown UI (adding searching ability)
`max_choices` param, can be set when `multiselect` is `True`
Adding A11y accessibility 
Some styling updates (hover over tokens)


Please include: 
* relevant motivation
* a summary of the change 
* which issue is fixed. 
* any additional dependencies that are required for this change.

Closes: #3184, #3163

# Checklist:

- [ ] I have performed a self-review of my own code
- [ ] I have added a short summary of my change to the CHANGELOG.md
- [ ] My code follows the style guidelines of this project
- [ ] I have commented my code in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes


# A note about the CHANGELOG

Hello 👋 and thank you for contributing to Gradio!

All pull requests must update the change log located in CHANGELOG.md, unless the pull request is labeled with the "no-changelog-update" label.

Please add a brief summary of the change to the Upcoming Release > Full Changelog section of the CHANGELOG.md file and include
a link to the PR (formatted in markdown) and a link to your github profile (if you like). For example, "* Added a cool new feature by `[@myusername](link-to-your-github-profile)` in `[PR 11111](https://github.com/gradio-app/gradio/pull/11111)`".

If you would like to elaborate on your change further, feel free to include a longer explanation in the other sections.
If you would like an image/gif/video showcasing your feature, it may be best to edit the CHANGELOG file using the 
GitHub web UI since that lets you upload files directly via drag-and-drop.

Problem statement:
Limit choices for `gr.Dropdown` multiselect 
- [x] I have searched to see if a similar issue already exists.

**Is your feature request related to a problem? Please describe.**  
I would like to limit to a certain number of choices for the multiselect option

**Describe the solution you'd like**  
I would like to limit the user to choose only a fixed amount of options out of all, so say a `maximumChoices` parameter that can be set to a value and then users can only select up to that amount of options.

Hints from issue discussion:
An odd additional request: 
If we implement something like a `maximumChoices` parameter, please have it start with `1`. 

It know it sounds weird and would be more logical that if `multiselect` == `True`, a `maximumChoices` parameter should start from `>=2`, but this would be a way to allow for users to use the amazing *searchable UI* of `multiselect` even if they only need 1 option selected.

An alternative is a `searchable` param to be set to `True` even if `multiselect` is `False`
Makes sense to me! cc @dawoodkhan82 
When we implement this, I think we should just make the "regular" Dropdown an alias for multiselect with maximum_choices=1 and that way all dropdowns will be searchable.
I like that (and agree that the regular `Dropdown` should be searchable) but the only issue is that the Dropdown passes in the selected value as a string, whereas if `multiselect=True`, the selected values are always passed in as a list
Makes sense. I'm sure I can make it so that the regular dropdown uses the `Multiselect` UI but still returns a string, so it won't be a breaking change.

Commit message: format

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58e095b1c0..c5fde6cbe3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,16 @@
 # Upcoming Release
 
 ## New Features:
+
+### Dropdown Component Updates
+
+The standard dropdown component now supports searching for choices. Also when `multiselect` is `True`, you can specify `max_choices` to set the maximum number of choices you want the user to be able to select from the dropdown component.
+
+```python
+gr.Dropdown(label="Multiselect Dropdown", choices=CHOICES, multiselect=True, max_choices=2),
+```
+by [@dawoodkhan82](https://github.com/dawoodkhan82) in [PR 3211](https://github.com/gradio-app/gradio/pull/3211)
+
 - Updated image upload component to accept all image formats, including lossless formats like .webp by [@fienestar](https://github.com/fienestar) in [PR 3225](https://github.com/gradio-app/gradio/pull/3225)
 - Adds a disabled mode to the `gr.Button` component by setting `interactive=False` by [@abidlabs](https://github.com/abidlabs) in [PR 3266](https://github.com/gradio-app/gradio/pull/3266) and [PR 3288](https://github.com/gradio-app/gradio/pull/3288)
 - Allow the setting of `brush_radius` for the `Image` component both as a default and via `Image.update()` by [@pngwn](https://github.com/pngwn) in [PR 3277](https://github.com/gradio-app/gradio/pull/3277)
diff --git a/demo/kitchen_sink/run.ipynb b/demo/kitchen_sink/run.ipynb
index 7131e5e337..c61c3aa5dc 100644
--- a/demo/kitchen_sink/run.ipynb
+++ b/demo/kitchen_sink/run.ipynb
@@ -1 +1 @@
-{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: kitchen_sink"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('files')\n", "!wget -q -O files/cantina.wav https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/cantina.wav\n", "!wget -q -O files/cheetah1.jpg https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/cheetah1.jpg\n", "!wget -q -O files/time.csv https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/time.csv\n", "!wget -q -O files/titanic.csv https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/titanic.csv\n", "!wget -q -O files/world.mp4 https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/world.mp4"]}, {"cell_type": "code", "execution_count": null, "id": 44380577570523278879349135829904343037, "metadata": {}, "outputs": [], "source": ["import os\n", "import json\n", "\n", "import numpy as np\n", "\n", "import gradio as gr\n", "\n", "CHOICES = [\"foo\", \"bar\", \"baz\"]\n", "JSONOBJ = \"\"\"{\"items\":{\"item\":[{\"id\": \"0001\",\"type\": null,\"is_good\": false,\"ppu\": 0.55,\"batters\":{\"batter\":[{ \"id\": \"1001\", \"type\": \"Regular\" },{ \"id\": \"1002\", \"type\": \"Chocolate\" },{ \"id\": \"1003\", \"type\": \"Blueberry\" },{ \"id\": \"1004\", \"type\": \"Devil's Food\" }]},\"topping\":[{ \"id\": \"5001\", \"type\": \"None\" },{ \"id\": \"5002\", \"type\": \"Glazed\" },{ \"id\": \"5005\", \"type\": \"Sugar\" },{ \"id\": \"5007\", \"type\": \"Powdered Sugar\" },{ \"id\": \"5006\", \"type\": \"Chocolate with Sprinkles\" },{ \"id\": \"5003\", \"type\": \"Chocolate\" },{ \"id\": \"5004\", \"type\": \"Maple\" }]}]}}\"\"\"\n", "\n", "\n", "def fn(\n", "    text1,\n", "    text2,\n", "    num,\n", "    slider1,\n", "    slider2,\n", "    single_checkbox,\n", "    checkboxes,\n", "    radio,\n", "    dropdown,\n", "    im1,\n", "    im2,\n", "    im3,\n", "    im4,\n", "    video,\n", "    audio1,\n", "    audio2,\n", "    file,\n", "    df1,\n", "    df2,\n", "):\n", "    return (\n", "        (text1 if single_checkbox else text2)\n", "        + \", selected:\"\n", "        + \", \".join(checkboxes),  # Text\n", "        {\n", "            \"positive\": num / (num + slider1 + slider2),\n", "            \"negative\": slider1 / (num + slider1 + slider2),\n", "            \"neutral\": slider2 / (num + slider1 + slider2),\n", "        },  # Label\n", "        (audio1[0], np.flipud(audio1[1]))\n", "        if audio1 is not None\n", "        else os.path.join(os.path.abspath(''), \"files/cantina.wav\"),  # Audio\n", "        np.flipud(im1)\n", "        if im1 is not None\n", "        else os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),  # Image\n", "        video\n", "        if video is not None\n", "        else os.path.join(os.path.abspath(''), \"files/world.mp4\"),  # Video\n", "        [\n", "            (\"The\", \"art\"),\n", "            (\"quick brown\", \"adj\"),\n", "            (\"fox\", \"nn\"),\n", "            (\"jumped\", \"vrb\"),\n", "            (\"testing testing testing\", None),\n", "            (\"over\", \"prp\"),\n", "            (\"the\", \"art\"),\n", "            (\"testing\", None),\n", "            (\"lazy\", \"adj\"),\n", "            (\"dogs\", \"nn\"),\n", "            (\".\", \"punc\"),\n", "        ]\n", "        + [(f\"test {x}\", f\"test {x}\") for x in range(10)],  # HighlightedText\n", "        # [(\"The testing testing testing\", None), (\"quick brown\", 0.2), (\"fox\", 1), (\"jumped\", -1), (\"testing testing testing\", 0), (\"over\", 0), (\"the\", 0), (\"testing\", 0), (\"lazy\", 1), (\"dogs\", 0), (\".\", 1)] + [(f\"test {x}\",  x/10) for x in range(-10, 10)],  # HighlightedText\n", "        [\n", "            (\"The testing testing testing\", None),\n", "            (\"over\", 0.6),\n", "            (\"the\", 0.2),\n", "            (\"testing\", None),\n", "            (\"lazy\", -0.1),\n", "            (\"dogs\", 0.4),\n", "            (\".\", 0),\n", "        ]\n", "        + [(f\"test\", x / 10) for x in range(-10, 10)],  # HighlightedText\n", "        json.loads(JSONOBJ),  # JSON\n", "        \"<button style='background-color: red'>Click Me: \"\n", "        + radio\n", "        + \"</button>\",  # HTML\n", "        os.path.join(os.path.abspath(''), \"files/titanic.csv\"),\n", "        df1,  # Dataframe\n", "        np.random.randint(0, 10, (4, 4)),  # Dataframe\n", "        df2,  # Timeseries\n", "    )\n", "\n", "\n", "demo = gr.Interface(\n", "    fn,\n", "    inputs=[\n", "        gr.Textbox(value=\"Lorem ipsum\", label=\"Textbox\"),\n", "        gr.Textbox(lines=3, placeholder=\"Type here..\", label=\"Textbox 2\"),\n", "        gr.Number(label=\"Number\", value=42),\n", "        gr.Slider(10, 20, value=15, label=\"Slider: 10 - 20\"),\n", "        gr.Slider(maximum=20, step=0.04, label=\"Slider: step @ 0.04\"),\n", "        gr.Checkbox(label=\"Checkbox\"),\n", "        gr.CheckboxGroup(label=\"CheckboxGroup\", choices=CHOICES, value=CHOICES[0:2]),\n", "        gr.Radio(label=\"Radio\", choices=CHOICES, value=CHOICES[2]),\n", "        gr.Dropdown(label=\"Dropdown\", choices=CHOICES),\n", "        gr.Image(label=\"Image\"),\n", "        gr.Image(label=\"Image w/ Cropper\", tool=\"select\"),\n", "        gr.Image(label=\"Sketchpad\", source=\"canvas\"),\n", "        gr.Image(label=\"Webcam\", source=\"webcam\"),\n", "        gr.Video(label=\"Video\"),\n", "        gr.Audio(label=\"Audio\"),\n", "        gr.Audio(label=\"Microphone\", source=\"microphone\"),\n", "        gr.File(label=\"File\"),\n", "        gr.Dataframe(label=\"Dataframe\", headers=[\"Name\", \"Age\", \"Gender\"]),\n", "        gr.Timeseries(x=\"time\", y=[\"price\", \"value\"], colors=[\"pink\", \"purple\"]),\n", "    ],\n", "    outputs=[\n", "        gr.Textbox(label=\"Textbox\"),\n", "        gr.Label(label=\"Label\"),\n", "        gr.Audio(label=\"Audio\"),\n", "        gr.Image(label=\"Image\"),\n", "        gr.Video(label=\"Video\"),\n", "        gr.HighlightedText(label=\"HighlightedText\").style(\n", "            color_map={\"punc\": \"pink\", \"test 0\": \"blue\"}\n", "        ),\n", "        gr.HighlightedText(label=\"HighlightedText\", show_legend=True),\n", "        gr.JSON(label=\"JSON\"),\n", "        gr.HTML(label=\"HTML\"),\n", "        gr.File(label=\"File\"),\n", "        gr.Dataframe(label=\"Dataframe\"),\n", "        gr.Dataframe(label=\"Numpy\"),\n", "        gr.Timeseries(x=\"time\", y=[\"price\", \"value\"], label=\"Timeseries\"),\n", "    ],\n", "    examples=[\n", "        [\n", "            \"the quick brown fox\",\n", "            \"jumps over the lazy dog\",\n", "            10,\n", "            12,\n", "            4,\n", "            True,\n", "            [\"foo\", \"baz\"],\n", "            \"baz\",\n", "            \"bar\",\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/world.mp4\"),\n", "            os.path.join(os.path.abspath(''), \"files/cantina.wav\"),\n", "            os.path.join(os.path.abspath(''), \"files/cantina.wav\"),\n", "            os.path.join(os.path.abspath(''), \"files/titanic.csv\"),\n", "            [[1, 2, 3, 4], [4, 5, 6, 7], [8, 9, 1, 2], [3, 4, 5, 6]],\n", "            os.path.join(os.path.abspath(''), \"files/time.csv\"),\n", "        ]\n", "    ]\n", "    * 3,\n", "    theme=\"default\",\n", "    title=\"Kitchen Sink\",\n", "    description=\"Try out all the components!\",\n", "    article=\"Learn more about [Gradio](http://gradio.app)\",\n", "    cache_examples=True,\n", ")\n", "\n", "if __name__ == \"__main__\":\n", "    demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
+{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: kitchen_sink"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "os.mkdir('files')\n", "!wget -q -O files/cantina.wav https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/cantina.wav\n", "!wget -q -O files/cheetah1.jpg https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/cheetah1.jpg\n", "!wget -q -O files/time.csv https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/time.csv\n", "!wget -q -O files/titanic.csv https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/titanic.csv\n", "!wget -q -O files/world.mp4 https://github.com/gradio-app/gradio/raw/main/demo/kitchen_sink/files/world.mp4"]}, {"cell_type": "code", "execution_count": null, "id": 44380577570523278879349135829904343037, "metadata": {}, "outputs": [], "source": ["import os\n", "import json\n", "\n", "import numpy as np\n", "\n", "import gradio as gr\n", "\n", "CHOICES = [\"foo\", \"bar\", \"baz\"]\n", "JSONOBJ = \"\"\"{\"items\":{\"item\":[{\"id\": \"0001\",\"type\": null,\"is_good\": false,\"ppu\": 0.55,\"batters\":{\"batter\":[{ \"id\": \"1001\", \"type\": \"Regular\" },{ \"id\": \"1002\", \"type\": \"Chocolate\" },{ \"id\": \"1003\", \"type\": \"Blueberry\" },{ \"id\": \"1004\", \"type\": \"Devil's Food\" }]},\"topping\":[{ \"id\": \"5001\", \"type\": \"None\" },{ \"id\": \"5002\", \"type\": \"Glazed\" },{ \"id\": \"5005\", \"type\": \"Sugar\" },{ \"id\": \"5007\", \"type\": \"Powdered Sugar\" },{ \"id\": \"5006\", \"type\": \"Chocolate with Sprinkles\" },{ \"id\": \"5003\", \"type\": \"Chocolate\" },{ \"id\": \"5004\", \"type\": \"Maple\" }]}]}}\"\"\"\n", "\n", "\n", "def fn(\n", "    text1,\n", "    text2,\n", "    num,\n", "    slider1,\n", "    slider2,\n", "    single_checkbox,\n", "    checkboxes,\n", "    radio,\n", "    dropdown,\n", "    multi_dropdown,\n", "    im1,\n", "    im2,\n", "    im3,\n", "    im4,\n", "    video,\n", "    audio1,\n", "    audio2,\n", "    file,\n", "    df1,\n", "    df2,\n", "):\n", "    return (\n", "        (text1 if single_checkbox else text2)\n", "        + \", selected:\"\n", "        + \", \".join(checkboxes),  # Text\n", "        {\n", "            \"positive\": num / (num + slider1 + slider2),\n", "            \"negative\": slider1 / (num + slider1 + slider2),\n", "            \"neutral\": slider2 / (num + slider1 + slider2),\n", "        },  # Label\n", "        (audio1[0], np.flipud(audio1[1]))\n", "        if audio1 is not None\n", "        else os.path.join(os.path.abspath(''), \"files/cantina.wav\"),  # Audio\n", "        np.flipud(im1)\n", "        if im1 is not None\n", "        else os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),  # Image\n", "        video\n", "        if video is not None\n", "        else os.path.join(os.path.abspath(''), \"files/world.mp4\"),  # Video\n", "        [\n", "            (\"The\", \"art\"),\n", "            (\"quick brown\", \"adj\"),\n", "            (\"fox\", \"nn\"),\n", "            (\"jumped\", \"vrb\"),\n", "            (\"testing testing testing\", None),\n", "            (\"over\", \"prp\"),\n", "            (\"the\", \"art\"),\n", "            (\"testing\", None),\n", "            (\"lazy\", \"adj\"),\n", "            (\"dogs\", \"nn\"),\n", "            (\".\", \"punc\"),\n", "        ]\n", "        + [(f\"test {x}\", f\"test {x}\") for x in range(10)],  # HighlightedText\n", "        # [(\"The testing testing testing\", None), (\"quick brown\", 0.2), (\"fox\", 1), (\"jumped\", -1), (\"testing testing testing\", 0), (\"over\", 0), (\"the\", 0), (\"testing\", 0), (\"lazy\", 1), (\"dogs\", 0), (\".\", 1)] + [(f\"test {x}\",  x/10) for x in range(-10, 10)],  # HighlightedText\n", "        [\n", "            (\"The testing testing testing\", None),\n", "            (\"over\", 0.6),\n", "            (\"the\", 0.2),\n", "            (\"testing\", None),\n", "            (\"lazy\", -0.1),\n", "            (\"dogs\", 0.4),\n", "            (\".\", 0),\n", "        ]\n", "        + [(f\"test\", x / 10) for x in range(-10, 10)],  # HighlightedText\n", "        json.loads(JSONOBJ),  # JSON\n", "        \"<button style='background-color: red'>Click Me: \"\n", "        + radio\n", "        + \"</button>\",  # HTML\n", "        os.path.join(os.path.abspath(''), \"files/titanic.csv\"),\n", "        df1,  # Dataframe\n", "        np.random.randint(0, 10, (4, 4)),  # Dataframe\n", "        df2,  # Timeseries\n", "    )\n", "\n", "\n", "demo = gr.Interface(\n", "    fn,\n", "    inputs=[\n", "        gr.Textbox(value=\"Lorem ipsum\", label=\"Textbox\"),\n", "        gr.Textbox(lines=3, placeholder=\"Type here..\", label=\"Textbox 2\"),\n", "        gr.Number(label=\"Number\", value=42),\n", "        gr.Slider(10, 20, value=15, label=\"Slider: 10 - 20\"),\n", "        gr.Slider(maximum=20, step=0.04, label=\"Slider: step @ 0.04\"),\n", "        gr.Checkbox(label=\"Checkbox\"),\n", "        gr.CheckboxGroup(label=\"CheckboxGroup\", choices=CHOICES, value=CHOICES[0:2]),\n", "        gr.Radio(label=\"Radio\", choices=CHOICES, value=CHOICES[2]),\n", "        gr.Dropdown(label=\"Dropdown\", choices=CHOICES),\n", "        gr.Dropdown(label=\"Multiselect Dropdown (Max choice: 2)\", choices=CHOICES, multiselect=True, max_choices=2),\n", "        gr.Image(label=\"Image\"),\n", "        gr.Image(label=\"Image w/ Cropper\", tool=\"select\"),\n", "        gr.Image(label=\"Sketchpad\", source=\"canvas\"),\n", "        gr.Image(label=\"Webcam\", source=\"webcam\"),\n", "        gr.Video(label=\"Video\"),\n", "        gr.Audio(label=\"Audio\"),\n", "        gr.Audio(label=\"Microphone\", source=\"microphone\"),\n", "        gr.File(label=\"File\"),\n", "        gr.Dataframe(label=\"Dataframe\", headers=[\"Name\", \"Age\", \"Gender\"]),\n", "        gr.Timeseries(x=\"time\", y=[\"price\", \"value\"], colors=[\"pink\", \"purple\"]),\n", "    ],\n", "    outputs=[\n", "        gr.Textbox(label=\"Textbox\"),\n", "        gr.Label(label=\"Label\"),\n", "        gr.Audio(label=\"Audio\"),\n", "        gr.Image(label=\"Image\"),\n", "        gr.Video(label=\"Video\"),\n", "        gr.HighlightedText(label=\"HighlightedText\").style(\n", "            color_map={\"punc\": \"pink\", \"test 0\": \"blue\"}\n", "        ),\n", "        gr.HighlightedText(label=\"HighlightedText\", show_legend=True),\n", "        gr.JSON(label=\"JSON\"),\n", "        gr.HTML(label=\"HTML\"),\n", "        gr.File(label=\"File\"),\n", "        gr.Dataframe(label=\"Dataframe\"),\n", "        gr.Dataframe(label=\"Numpy\"),\n", "        gr.Timeseries(x=\"time\", y=[\"price\", \"value\"], label=\"Timeseries\"),\n", "    ],\n", "    examples=[\n", "        [\n", "            \"the quick brown fox\",\n", "            \"jumps over the lazy dog\",\n", "            10,\n", "            12,\n", "            4,\n", "            True,\n", "            [\"foo\", \"baz\"],\n", "            \"baz\",\n", "            \"bar\",\n", "            [\"foo\", \"bar\"],\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/cheetah1.jpg\"),\n", "            os.path.join(os.path.abspath(''), \"files/world.mp4\"),\n", "            os.path.join(os.path.abspath(''), \"files/cantina.wav\"),\n", "            os.path.join(os.path.abspath(''), \"files/cantina.wav\"),\n", "            os.path.join(os.path.abspath(''), \"files/titanic.csv\"),\n", "            [[1, 2, 3, 4], [4, 5, 6, 7], [8, 9, 1, 2], [3, 4, 5, 6]],\n", "            os.path.join(os.path.abspath(''), \"files/time.csv\"),\n", "        ]\n", "    ]\n", "    * 3,\n", "    theme=\"default\",\n", "    title=\"Kitchen Sink\",\n", "    description=\"Try out all the components!\",\n", "    article=\"Learn more about [Gradio](http://gradio.app)\",\n", "    cache_examples=True,\n", ")\n", "\n", "if __name__ == \"__main__\":\n", "    demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
diff --git a/demo/kitchen_sink/run.py b/demo/kitchen_sink/run.py
index ebefc51737..53e5a94634 100755
--- a/demo/kitchen_sink/run.py
+++ b/demo/kitchen_sink/run.py
@@ -19,6 +19,7 @@ def fn(
     checkboxes,
     radio,
     dropdown,
+    multi_dropdown,
     im1,
     im2,
     im3,
@@ -96,6 +97,7 @@ def fn(
         gr.CheckboxGroup(label="CheckboxGroup", choices=CHOICES, value=CHOICES[0:2]),
         gr.Radio(label="Radio", choices=CHOICES, value=CHOICES[2]),
         gr.Dropdown(label="Dropdown", choices=CHOICES),
+        gr.Dropdown(label="Multiselect Dropdown (Max choice: 2)", choices=CHOICES, multiselect=True, max_choices=2),
         gr.Image(label="Image"),
         gr.Image(label="Image w/ Cropper", tool="select"),
         gr.Image(label="Sketchpad", source="canvas"),
@@ -135,6 +137,7 @@ def fn(
             ["foo", "baz"],
             "baz",
             "bar",
+            ["foo", "bar"],
             os.path.join(os.path.dirname(__file__), "files/cheetah1.jpg"),
             os.path.join(os.path.dirname(__file__), "files/cheetah1.jpg"),
             os.path.join(os.path.dirname(__file__), "files/cheetah1.jpg"),
diff --git a/gradio/components.py b/gradio/components.py
index fb7cb9dc5e..a3a34b6f3d 100644
--- a/gradio/components.py
+++ b/gradio/components.py
@@ -1204,6 +1204,7 @@ def __init__(
         value: str | List[str] | Callable | None = None,
         type: str = "value",
         multiselect: bool | None = None,
+        max_choices: int | None = None,
         label: str | None = None,
         info: str | None = None,
         every: float | None = None,
@@ -1219,6 +1220,7 @@ def __init__(
             value: default value(s) selected in dropdown. If None, no value is selected by default. If callable, the function will be called whenever the app loads to set the initial value of the component.
             type: Type of value to be returned by component. "value" returns the string of the choice selected, "index" returns the index of the choice selected.
             multiselect: if True, multiple choices can be selected.
+            max_choices: maximum number of choices that can be selected. If None, no limit is enforced.
             label: component name in interface.
             info: additional component description.
             every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.
@@ -1238,6 +1240,9 @@ def __init__(
         if multiselect:
             if isinstance(value, str):
                 value = [value]
+        if not multiselect and max_choices is not None:
+            warnings.warn("Max choices is ignored when multiselect is False.")
+        self.max_choices = max_choices
         self.test_input = self.choices[0] if len(self.choices) else None
         self.interpret_by_tokens = False
         IOComponent.__init__(
@@ -1259,6 +1264,7 @@ def get_config(self):
             "choices": self.choices,
             "value": self.value,
             "multiselect": self.multiselect,
+            "max_choices": self.max_choices,
             **IOComponent.get_config(self),
         }
 
diff --git a/test/test_components.py b/test/test_components.py
index 20a80d4602..1291ad5ded 100644
--- a/test/test_components.py
+++ b/test/test_components.py
@@ -563,7 +563,7 @@ def test_component_functions(self):
         assert dropdown_input.preprocess("a") == "a"
         assert dropdown_input.postprocess("a") == "a"
 
-        dropdown_input_multiselect = gr.Dropdown(["a", "b", "c"], multiselect=True)
+        dropdown_input_multiselect = gr.Dropdown(["a", "b", "c"])
         assert dropdown_input_multiselect.preprocess(["a", "c"]) == ["a", "c"]
         assert dropdown_input_multiselect.postprocess(["a", "c"]) == ["a", "c"]
         assert dropdown_input_multiselect.serialize(["a", "c"], True) == ["a", "c"]
@@ -572,6 +572,8 @@ def test_component_functions(self):
             value=["a", "c"],
             choices=["a", "b", "c"],
             label="Select Your Inputs",
+            multiselect=True,
+            max_choices=2,
         )
         assert dropdown_input_multiselect.get_config() == {
             "choices": ["a", "b", "c"],
@@ -584,7 +586,8 @@ def test_component_functions(self):
             "visible": True,
             "interactive": None,
             "root_url": None,
-            "multiselect": None,
+            "multiselect": True,
+            "max_choices": 2,
         }
         with pytest.raises(ValueError):
             gr.Dropdown(["a"], type="unknown")
diff --git a/ui/packages/app/src/components/Dropdown/Dropdown.svelte b/ui/packages/app/src/components/Dropdown/Dropdown.svelte
index 3032c4a155..e2082dcef3 100644
--- a/ui/packages/app/src/components/Dropdown/Dropdown.svelte
+++ b/ui/packages/app/src/components/Dropdown/Dropdown.svelte
@@ -11,6 +11,7 @@
 	export let visible: boolean = true;
 	export let value: string | Array<string> = [];
 	export let multiselect: boolean = false;
+	export let max_choices: number;
 	export let choices: Array<string>;
 	export let show_label: boolean;
 	export let style: Styles = {};
@@ -30,6 +31,7 @@
 		bind:value
 		{choices}
 		{multiselect}
+		{max_choices}
 		{label}
 		{info}
 		{show_label}
diff --git a/ui/packages/app/test/outbreak_forecast.spec.ts b/ui/packages/app/test/outbreak_forecast.spec.ts
index 3184d83bec..4d6020cba9 100644
--- a/ui/packages/app/test/outbreak_forecast.spec.ts
+++ b/ui/packages/app/test/outbreak_forecast.spec.ts
@@ -31,8 +31,10 @@ test("matplotlib", async ({ page }) => {
 	await mock_api(page, [[{ type: "matplotlib", plot: BASE64_PLOT_IMG }]]);
 	await page.goto("http://localhost:9876");
 
-	await page.getByLabel("Plot Type").selectOption("Matplotlib");
-	await page.getByLabel("Month").selectOption("January");
+	await page.getByLabel("Plot Type").click();
+	await page.getByRole("button", { name: "Matplotlib" }).click();
+	await page.getByLabel("Month").click();
+	await page.getByRole("button", { name: "January" }).click();
 	await page.getByLabel("Social Distancing?").check();
 
 	await Promise.all([
@@ -57,8 +59,10 @@ test("plotly", async ({ page }) => {
 	]);
 	await page.goto("http://localhost:9876");
 
-	await page.getByLabel("Plot Type").selectOption("Plotly");
-	await page.getByLabel("Month").selectOption("January");
+	await page.getByLabel("Plot Type").click();
+	await page.getByRole("button", { name: "Matplotlib" }).click();
+	await page.getByLabel("Month").click();
+	await page.getByRole("button", { name: "January" }).click();
 	await page.getByLabel("Social Distancing?").check();
 
 	await Promise.all([
diff --git a/ui/packages/form/package.json b/ui/packages/form/package.json
index e108ae2721..1df7dc4bfd 100644
--- a/ui/packages/form/package.json
+++ b/ui/packages/form/package.json
@@ -9,6 +9,7 @@
 	"private": true,
 	"dependencies": {
 		"@gradio/atoms": "workspace:^0.0.1",
-		"@gradio/utils": "workspace:^0.0.1"
+		"@gradio/utils": "workspace:^0.0.1",
+		"@gradio/icons": "workspace:^0.0.1"
 	}
 }
diff --git a/ui/packages/form/src/Dropdown.svelte b/ui/packages/form/src/Dropdown.svelte
index 7a93ee34fd..cb1ccf719a 100644
--- a/ui/packages/form/src/Dropdown.svelte
+++ b/ui/packages/form/src/Dropdown.svelte
@@ -1,70 +1,270 @@
 <script lang="ts">
-	import MultiSelect from "./MultiSelect.svelte";
+	import DropdownOptions from "./DropdownOptions.svelte";
 	import { createEventDispatcher } from "svelte";
 	import { BlockTitle } from "@gradio/atoms";
+	import { Remove, DropdownArrow } from "@gradio/icons";
 	export let label: string;
 	export let info: string | undefined = undefined;
 	export let value: string | Array<string> | undefined = undefined;
 	export let multiselect: boolean = false;
+	export let max_choices: number;
 	export let choices: Array<string>;
 	export let disabled: boolean = false;
 	export let show_label: boolean;
 
 	const dispatch = createEventDispatcher<{
-		change: string | Array<string>;
+		change: string | Array<string> | undefined;
 	}>();
 
+	let inputValue: string,
+		activeOption: string,
+		showOptions = false;
+
+	$: filtered = choices.filter((o) =>
+		inputValue ? o.toLowerCase().includes(inputValue.toLowerCase()) : o
+	);
+	$: if (
+		(activeOption && !filtered.includes(activeOption)) ||
+		(!activeOption && inputValue)
+	)
+		activeOption = filtered[0];
+
+	$: readonly =
+		(!multiselect && typeof value === "string") ||
+		(multiselect && Array.isArray(value) && value.length === max_choices);
+
 	// The initial value of value is [] so that can
 	// cause infinite loops in the non-multiselect case
 	$: if (!multiselect && !Array.isArray(value)) {
 		dispatch("change", value);
 	}
+
+	function add(option: string) {
+		if (Array.isArray(value)) {
+			if (value.length < max_choices) {
+				value.push(option);
+				dispatch("change", value);
+			}
+			showOptions = !(value.length === max_choices);
+		}
+		value = value;
+	}
+
+	function remove(option: string) {
+		if (Array.isArray(value)) {
+			value = value.filter((v: string) => v !== option);
+			dispatch("change", value);
+		}
+	}
+
+	function remove_all(e: any) {
+		value = [];
+		inputValue = "";
+		e.preventDefault();
+		dispatch("change", value);
+	}
+
+	function handleOptionMousedown(e: any) {
+		const option = e.detail.target.dataset.value;
+		inputValue = "";
+
+		if (option !== undefined) {
+			if (!multiselect) {
+				value = option;
+				inputValue = "";
+				dispatch("change", value);
+				return;
+			}
+			if (value?.includes(option)) {
+				remove(option);
+			} else {
+				add(option);
+			}
+		}
+	}
+
+	function handleKeyup(e: any) {
+		if (e.key === "Enter" && activeOption != undefined) {
+			if (!multiselect) {
+				value = activeOption;
+				inputValue = "";
+			} else if (multiselect && Array.isArray(value)) {
+				value.includes(activeOption) ? remove(activeOption) : add(activeOption);
+				inputValue = "";
+			}
+		}
+		if (e.key === "ArrowUp" || e.key === "ArrowDown") {
+			const increment = e.key === "ArrowUp" ? -1 : 1;
+			const calcIndex = filtered.indexOf(activeOption) + increment;
+			activeOption =
+				calcIndex < 0
+					? filtered[filtered.length - 1]
+					: calcIndex === filtered.length
+					? filtered[0]
+					: filtered[calcIndex];
+		}
+		if (e.key === "Escape") {
+			showOptions = false;
+		}
+	}
 </script>
 
 <!-- svelte-ignore a11y-label-has-associated-control -->
 <label>
 	<BlockTitle {show_label} {info}>{label}</BlockTitle>
 
-	{#if !multiselect}
-		<select bind:value {disabled}>
-			{#each choices as choice}
-				<option>{choice}</option>
-			{/each}
-		</select>
-	{:else}
-		<MultiSelect bind:value {choices} on:change {disabled} />
-	{/if}
+	<div class="wrap">
+		<div class="wrap-inner" class:showOptions>
+			{#if Array.isArray(value)}
+				{#each value as s}
+					<div on:click|preventDefault={() => remove(s)} class="token">
+						<span>{s}</span>
+						<div
+							class:hidden={disabled}
+							class="token-remove"
+							title="Remove {s}"
+						>
+							<Remove />
+						</div>
+					</div>
+				{/each}
+			{:else}
+				<span class="single-select">{value}</span>
+			{/if}
+			<div class="secondary-wrap">
+				<input
+					class="border-none"
+					{disabled}
+					{readonly}
+					autocomplete="off"
+					bind:value={inputValue}
+					on:focus={() =>
+						(showOptions =
+							Array.isArray(value) && value.length === max_choices
+								? false
+								: true)}
+					on:blur={() => (showOptions = false)}
+					on:keyup={handleKeyup}
+				/>
+				<div
+					class:hide={!value?.length || disabled}
+					class="token-remove remove-all"
+					title="Remove All"
+					on:click={remove_all}
+				>
+					<Remove />
+				</div>
+				<DropdownArrow />
+			</div>
+		</div>
+		<DropdownOptions
+			bind:value
+			{showOptions}
+			{filtered}
+			{activeOption}
+			{disabled}
+			on:change={handleOptionMousedown}
+		/>
+	</div>
 </label>
 
 <style>
-	select {
+	.wrap {
 		--ring-color: transparent;
-		display: block;
 		position: relative;
-		outline: none !important;
 		box-shadow: 0 0 0 var(--shadow-spread) var(--ring-color),
 			var(--shadow-inset);
-		border: 1px solid var(--input-border-color-base);
+		border: 1px solid var(--color-border-primary);
 		border-radius: var(--radius-lg);
-		background-color: var(--input-background-base);
-		padding: var(--size-2-5);
-		width: 100%;
-		color: var(--color-text-body);
-		font-size: var(--scale-00);
-		line-height: var(--line-sm);
 	}
 
-	select:focus {
+	.wrap:focus-within {
 		--ring-color: var(--color-focus-ring);
 		border-color: var(--input-border-color-focus);
 	}
 
-	select::placeholder {
-		color: var(--color-text-placeholder);
+	.wrap-inner {
+		display: flex;
+		position: relative;
+		flex-wrap: wrap;
+		align-items: center;
 	}
 
-	select[disabled] {
+	.token {
+		display: flex;
+		align-items: center;
+		cursor: pointer;
+		margin: var(--size-1);
+		box-shadow: var(--shadow-drop);
+		border: 1px solid var(--checkbox-label-border-color-base);
+		border-radius: var(--radius-md);
+		background: var(--checkbox-label-background-base);
+		padding: var(--size-1-5) var(--size-3);
+		color: var(--color-text-body);
+		font-size: var(--scale-00);
+		line-height: var(--line-md);
+	}
+
+	.token > * + * {
+		margin-left: var(--size-2);
+	}
+
+	.token:hover {
+		border: 1px solid var(--icon_button-border-color-hover);
+		color: var(--color-text-label);
+	}
+
+	.token-remove {
+		fill: var(--color-text-body);
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		cursor: pointer;
+		border: 1px solid var(--color-border-primary);
+		border-radius: var(--radius-full);
+		background: var(--color-background-tertiary);
+		padding: var(--size-0-5);
+		width: 18px;
+		height: 18px;
+	}
+
+	.token-remove:hover,
+	.remove-all:hover {
+		border: 1px solid var(--icon_button-border-color-hover);
+		color: var(--color-text-label);
+	}
+
+	.single-select {
+		margin: var(--size-2);
+		color: var(--color-text-body);
+	}
+
+	.secondary-wrap {
+		display: flex;
+		flex: 1 1 0%;
+		align-items: center;
+		border: none;
+		min-width: min-content;
+	}
+
+	input {
+		outline: none;
+		border: none;
+		background: inherit;
+		padding: var(--size-2-5);
+		width: 100%;
+		color: var(--color-text-body);
+		font-size: var(--scale-00);
+	}
+
+	input:disabled {
 		cursor: not-allowed;
 		box-shadow: none;
 	}
+
+	.remove-all {
+		margin-left: var(--size-1);
+		width: 20px;
+		height: 20px;
+	}
 </style>
diff --git a/ui/packages/form/src/DropdownOptions.svelte b/ui/packages/form/src/DropdownOptions.svelte
new file mode 100644
index 0000000000..d7b6a755b1
--- /dev/null
+++ b/ui/packages/form/src/DropdownOptions.svelte
@@ -0,0 +1,73 @@
+<script lang="ts">
+	import { fly } from "svelte/transition";
+	import { createEventDispatcher } from "svelte";
+	export let value: string | Array<string> | undefined = undefined;
+	export let filtered: Array<string>;
+	export let showOptions: boolean = false;
+	export let activeOption: string;
+	export let disabled: boolean = false;
+
+	const dispatch = createEventDispatcher();
+</script>
+
+{#if showOptions && !disabled}
+	<ul
+		class="options"
+		aria-expanded={showOptions}
+		transition:fly={{ duration: 200, y: 5 }}
+		on:mousedown|preventDefault={(e) => dispatch("change", e)}
+	>
+		{#each filtered as choice}
+			<li
+				class="item"
+				role="button"
+				class:selected={value?.includes(choice)}
+				class:active={activeOption === choice}
+				class:bg-gray-100={activeOption === choice}
+				class:dark:bg-gray-600={activeOption === choice}
+				data-value={choice}
+				aria-label={choice}
+			>
+				<span class:hide={!value?.includes(choice)} class="inner-item pr-1">
+					✓
+				</span>
+				{choice}
+			</li>
+		{/each}
+	</ul>
+{/if}
+
+<style>
+	.options {
+		position: absolute;
+		z-index: var(--layer-5);
+		margin-left: 0;
+		box-shadow: var(--shadow-drop-lg);
+		border-radius: var(--radius-lg);
+		background: var(--color-background-primary);
+		width: var(--size-full);
+		max-height: var(--size-32);
+		overflow: auto;
+		color: var(--color-text-body);
+		list-style: none;
+	}
+
+	.item {
+		display: flex;
+		cursor: pointer;
+		padding: var(--size-2);
+	}
+
+	.item:hover,
+	.active {
+		background: var(--color-background-secondary);
+	}
+
+	.inner-item {
+		padding-right: var(--size-1);
+	}
+
+	.hide {
+		visibility: hidden;
+	}
+</style>
diff --git a/ui/packages/icons/src/DropdownArrow.svelte b/ui/packages/icons/src/DropdownArrow.svelte
new file mode 100644
index 0000000000..af9565c677
--- /dev/null
+++ b/ui/packages/icons/src/DropdownArrow.svelte
@@ -0,0 +1,17 @@
+<svg
+	class="dropdown-arrow"
+	xmlns="http://www.w3.org/2000/svg"
+	width="18"
+	height="18"
+	viewBox="0 0 18 18"
+>
+	<path d="M5 8l4 4 4-4z" />
+</svg>
+
+<style>
+	.dropdown-arrow {
+		fill: var(--color-text-body);
+		margin-right: var(--size-2);
+		width: var(--size-5);
+	}
+</style>
diff --git a/ui/packages/icons/src/Remove.svelte b/ui/packages/icons/src/Remove.svelte
new file mode 100644
index 0000000000..5c9374a009
--- /dev/null
+++ b/ui/packages/icons/src/Remove.svelte
@@ -0,0 +1,10 @@
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	width="16"
+	height="16"
+	viewBox="0 0 24 24"
+>
+	<path
+		d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
+	/>
+</svg>
diff --git a/ui/packages/icons/src/index.ts b/ui/packages/icons/src/index.ts
index 5fa3eee757..a604ab7a14 100644
--- a/ui/packages/icons/src/index.ts
+++ b/ui/packages/icons/src/index.ts
@@ -5,6 +5,7 @@ export { default as Chat } from "./Chat.svelte";
 export { default as Circle } from "./Circle.svelte";
 export { default as Clear } from "./Clear.svelte";
 export { default as Color } from "./Color.svelte";
+export { default as DropdownArrow } from "./DropdownArrow.svelte";
 export { default as Edit } from "./Edit.svelte";
 export { default as File } from "./File.svelte";
 export { default as Image } from "./Image.svelte";
@@ -15,6 +16,7 @@ export { default as Music } from "./Music.svelte";
 export { default as Pause } from "./Pause.svelte";
 export { default as Play } from "./Play.svelte";
 export { default as Plot } from "./Plot.svelte";
+export { default as Remove } from "./Remove.svelte";
 export { default as Sketch } from "./Sketch.svelte";
 export { default as Square } from "./Square.svelte";
 export { default as Table } from "./Table.svelte";
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 00a99ccec7..48478ed954 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -230,9 +230,11 @@ importers:
   packages/form:
     specifiers:
       '@gradio/atoms': workspace:^0.0.1
+      '@gradio/icons': workspace:^0.0.1
       '@gradio/utils': workspace:^0.0.1
     dependencies:
       '@gradio/atoms': link:../atoms
+      '@gradio/icons': link:../icons
       '@gradio/utils': link:../utils
 
   packages/gallery:


Code review comments:
diff_hunk: @@ -1238,6 +1240,9 @@ def __init__(
         if multiselect:
             if isinstance(value, str):
                 value = [value]
+        if not multiselect and max_choices is not None:
+            warnings.warn("Max choices is ignored when multiselect is False.")
path: gradio/components.py
text: @user1:
```suggestion
            warnings.warn("The `max_choices` parameter is ignored when `multiselect` is False.")
```

diff_hunk: @@ -1,6 +1,16 @@
 # Upcoming Release
 
 ## New Features:
+
+### Dropdown Component Updates
+
+The standard dropdown component now supports searching for choices. Also when `multiselect` is `True`, you can specify `max_choices` to set the maximum number of choices you want the user to be able to select from the dropdown component.
+
+```python
+gr.Dropdown(label="Multiselect Dropdown", choices=CHOICES, multiselect=True, max_choices=2),
path: CHANGELOG.md
text: @user1:
I would suggest just making this more of a complete example:

```suggestion
gr.Dropdown(label="Choose your favorite colors", choices=["red", "blue", "green", "yellow", "orange"], multiselect=True, max_choices=2),
```