diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm
index 1efcbb02cbc..3525b2240ba 100644
--- a/code/game/machinery/hologram.dm
+++ b/code/game/machinery/hologram.dm
@@ -263,7 +263,7 @@ Possible to do for anyone motivated enough:
if(A)
LAZYADD(callnames[A], I)
callnames -= get_area(src)
- var/result = input(usr, "Choose an area to call", "Holocall") as null|anything in sortNames(callnames)
+ var/result = tgui_input_list(usr, "Choose an area to call", "Holocall", sortNames(callnames))
if(QDELETED(usr) || !result || outgoing_call)
return
if(usr.loc == loc)
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index ef8bd903c37..43256f297a3 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -480,7 +480,7 @@ GLOBAL_PROTECT(admin_verbs_hideable)
set desc = "Cause an explosion of varying strength at your location."
var/list/choices = list("Small Bomb (1, 2, 3, 3)", "Medium Bomb (2, 3, 4, 4)", "Big Bomb (3, 5, 7, 5)", "Maxcap", "Custom Bomb")
- var/choice = input("What size explosion would you like to produce? NOTE: You can do all this rapidly and in an IC manner (using cruise missiles!) with the Config/Launch Supplypod verb. WARNING: These ignore the maxcap") as null|anything in choices
+ var/choice = tgui_input_list(usr, "What size explosion would you like to produce? NOTE: You can do all this rapidly and in an IC manner (using cruise missiles!) with the Config/Launch Supplypod verb. WARNING: These ignore the maxcap", "Drop Bomb", choices)
var/turf/epicenter = mob.loc
switch(choice)
diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui/tgui_input_list.dm
new file mode 100644
index 00000000000..242b69a9347
--- /dev/null
+++ b/code/modules/tgui/tgui_input_list.dm
@@ -0,0 +1,184 @@
+/**
+ * Creates a TGUI input list window and returns the user's response.
+ *
+ * This proc should be used to create alerts that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_list(mob/user, message, title, list/buttons, timeout = 0)
+ if (!user)
+ user = usr
+ if(!length(buttons))
+ return
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_list_input/input = new(user, message, title, buttons, timeout)
+ input.ui_interact(user)
+ input.wait()
+ if (input)
+ . = input.choice
+ qdel(input)
+
+/**
+ * Creates an asynchronous TGUI input list window with an associated callback.
+ *
+ * This proc should be used to create inputs that invoke a callback with the user's chosen option.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_list_async(mob/user, message, title, list/buttons, datum/callback/callback, timeout = 60 SECONDS)
+ if (!user)
+ user = usr
+ if(!length(buttons))
+ return
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_list_input/async/input = new(user, message, title, buttons, callback, timeout)
+ input.ui_interact(user)
+
+/**
+ * # tgui_list_input
+ *
+ * Datum used for instantiating and using a TGUI-controlled list input that prompts the user with
+ * a message and shows a list of selectable options
+ */
+/datum/tgui_list_input
+ /// The title of the TGUI window
+ var/title
+ /// The textual body of the TGUI window
+ var/message
+ /// The list of buttons (responses) provided on the TGUI window
+ var/list/buttons
+ /// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
+ var/list/buttons_map
+ /// The button that the user has pressed, null if no selection has been made
+ var/choice
+ /// The time at which the tgui_list_input was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the tgui_list_input, after which the window will close and delete itself.
+ var/timeout
+ /// Boolean field describing if the tgui_list_input was closed by the user.
+ var/closed
+
+/datum/tgui_list_input/New(mob/user, message, title, list/buttons, timeout)
+ src.title = title
+ src.message = message
+ src.buttons = list()
+ src.buttons_map = list()
+
+ // Gets rid of illegal characters
+ var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
+
+ for(var/i in buttons)
+ var/string_key = whitelistedWords.Replace("[i]", "")
+
+ src.buttons += string_key
+ src.buttons_map[string_key] = i
+
+
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_list_input/Destroy(force, ...)
+ SStgui.close_uis(src)
+ QDEL_NULL(buttons)
+ . = ..()
+
+/**
+ * Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_list_input/proc/wait()
+ while (!choice && !closed)
+ stoplag(1)
+
+/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ListInput")
+ ui.open()
+
+/datum/tgui_list_input/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_list_input/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tgui_list_input/ui_static_data(mob/user)
+ . = list(
+ "title" = title,
+ "message" = message,
+ "buttons" = buttons
+ )
+
+/datum/tgui_list_input/ui_data(mob/user)
+ . = list()
+ if(timeout)
+ .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
+
+/datum/tgui_list_input/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("choose")
+ if (!(params["choice"] in buttons))
+ return
+ choice = buttons_map[params["choice"]]
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ SStgui.close_uis(src)
+ closed = TRUE
+ return TRUE
+
+/**
+ * # async tgui_list_input
+ *
+ * An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_list_input/async
+ /// The callback to be invoked by the tgui_list_input upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, callback, timeout)
+ ..(user, title, message, buttons, timeout)
+ src.callback = callback
+
+/datum/tgui_list_input/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_list_input/async/ui_close(mob/user)
+ . = ..()
+ qdel(src)
+
+/datum/tgui_list_input/async/ui_act(action, list/params)
+ . = ..()
+ if (!. || choice == null)
+ return
+ callback.InvokeAsync(choice)
+ qdel(src)
+
+/datum/tgui_list_input/async/wait()
+ return
diff --git a/tgstation.dme b/tgstation.dme
index 784cee67f0a..a876c350498 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3200,6 +3200,7 @@
#include "code\modules\tgui\states.dm"
#include "code\modules\tgui\tgui.dm"
#include "code\modules\tgui\tgui_alert.dm"
+#include "code\modules\tgui\tgui_input_list.dm"
#include "code\modules\tgui\tgui_window.dm"
#include "code\modules\tgui\states\admin.dm"
#include "code\modules\tgui\states\always.dm"
diff --git a/tgui/packages/tgui/interfaces/ListInput.js b/tgui/packages/tgui/interfaces/ListInput.js
new file mode 100644
index 00000000000..571cc4d2232
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ListInput.js
@@ -0,0 +1,212 @@
+/**
+ * @file
+ * @copyright 2020 watermelon914 (https://github.com/watermelon914)
+ * @license MIT
+ */
+
+import { clamp01 } from 'common/math';
+import { useBackend, useLocalState } from '../backend';
+import { Box, Button, Flex, Section, Input } from '../components';
+import { Window } from '../layouts';
+
+const ARROW_KEY_UP = 38;
+const ARROW_KEY_DOWN = 40;
+
+let lastScrollTime = 0;
+
+export const ListInput = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ title,
+ message,
+ buttons,
+ timeout,
+ } = data;
+
+ // Search
+ const [showSearchBar, setShowSearchBar] = useLocalState(
+ context, 'search_bar', false);
+ const [displayedArray, setDisplayedArray] = useLocalState(
+ context, 'displayed_array', buttons);
+
+ // KeyPress
+ const [searchArray, setSearchArray] = useLocalState(
+ context, 'search_array', []);
+ const [searchIndex, setSearchIndex] = useLocalState(
+ context, 'search_index', 0);
+ const [lastCharCode, setLastCharCode] = useLocalState(
+ context, 'last_char_code', null);
+
+ // Selected Button
+ const [selectedButton, setSelectedButton] = useLocalState(
+ context, 'selected_button', buttons[0]);
+ return (
+
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a