diff --git a/baystation12.dme b/baystation12.dme
index 9f75b7451e2..4154609d277 100644
--- a/baystation12.dme
+++ b/baystation12.dme
@@ -172,6 +172,7 @@
 #include "code\_onclick\hud\animal.dm"
 #include "code\_onclick\hud\chorus.dm"
 #include "code\_onclick\hud\fullscreen.dm"
+#include "code\_onclick\hud\ghost.dm"
 #include "code\_onclick\hud\global_hud.dm"
 #include "code\_onclick\hud\global_hud_inf.dm"
 #include "code\_onclick\hud\gun_mode.dm"
@@ -2464,6 +2465,7 @@
 #include "code\modules\mob\observer\ghost\ghost.dm"
 #include "code\modules\mob\observer\ghost\login.dm"
 #include "code\modules\mob\observer\ghost\logout.dm"
+#include "code\modules\mob\observer\ghost\orbit.dm"
 #include "code\modules\mob\observer\ghost\say.dm"
 #include "code\modules\mob\observer\virtual\_constants.dm"
 #include "code\modules\mob\observer\virtual\base.dm"
diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm
index 543f420e6ea..167565f25ad 100644
--- a/code/_onclick/hud/_defines.dm
+++ b/code/_onclick/hud/_defines.dm
@@ -133,3 +133,11 @@
 #define ui_pai_light "NORTH,WEST+3:6"
 #define ui_pai_rest "NORTH,WEST+4:6"
 
+// Ghosts
+#define ui_ghost_toggle_darkness "SOUTH:6,CENTER-3:16"
+#define ui_ghost_jumptomob       "SOUTH:6,CENTER-2:16"
+#define ui_ghost_orbit           "SOUTH:6,CENTER-1:16"
+#define ui_ghost_reenter_corpse  "SOUTH:6,CENTER:16"
+#define ui_ghost_teleport        "SOUTH:6,CENTER+1:16"
+#define ui_ghost_mafia           "SOUTH:6,CENTER+2:16"
+#define ui_ghost_spawners_menu   "SOUTH:6,CENTER-4:16"
diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm
new file mode 100644
index 00000000000..d487d6bf772
--- /dev/null
+++ b/code/_onclick/hud/ghost.dm
@@ -0,0 +1,51 @@
+/obj/screen/ghost
+	icon = 'icons/hud/screen_ghost.dmi'
+
+/obj/screen/ghost/MouseExited(location, control, params)
+	. = ..()
+	flick(icon_state + "_anim", src)
+
+/obj/screen/ghost/jumptomob
+	name = "Jump to mob"
+	icon_state = "jumptomob"
+	screen_loc = ui_ghost_jumptomob
+
+/obj/screen/ghost/jumptomob/Click()
+	var/mob/observer/ghost/G = usr
+	G.jumptomob()
+
+/obj/screen/ghost/orbit
+	name = "Orbit"
+	icon_state = "orbit"
+	screen_loc = ui_ghost_orbit
+
+/obj/screen/ghost/orbit/Click()
+	var/mob/observer/ghost/G = usr
+	G.follow()
+
+/obj/screen/ghost/reenter_corpse
+	name = "Reenter corpse"
+	icon_state = "reenter_corpse"
+	screen_loc = ui_ghost_reenter_corpse
+
+/obj/screen/ghost/reenter_corpse/Click()
+	var/mob/observer/ghost/G = usr
+	G.reenter_corpse()
+
+/obj/screen/ghost/teleport
+	name = "Teleport"
+	icon_state = "teleport"
+	screen_loc = ui_ghost_teleport
+
+/obj/screen/ghost/teleport/Click()
+	var/mob/observer/ghost/G = usr
+	G.dead_tele()
+
+/obj/screen/ghost/toggle_darkness
+	name = "Toggle Darkness"
+	icon_state = "toggle_darkness"
+	screen_loc = ui_ghost_toggle_darkness
+
+/obj/screen/ghost/toggle_darkness/Click()
+	var/mob/observer/ghost/G = usr
+	G.toggle_darkness()
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index 047510e4eaf..2d353567ca2 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -191,6 +191,7 @@
 			if(!client.holder && !config.antag_hud_allowed)           // For new ghosts we remove the verb from even showing up if it's not allowed.
 				observer.verbs -= /mob/observer/ghost/verb/toggle_antagHUD        // Poor guys, don't know what they are missing!
 			observer.key = key
+			observer.add_ghost_buttons()
 			qdel(src)
 
 			return 1
diff --git a/code/modules/mob/observer/following.dm b/code/modules/mob/observer/following.dm
index 6ef4ae13b3c..bf53b5bc670 100644
--- a/code/modules/mob/observer/following.dm
+++ b/code/modules/mob/observer/following.dm
@@ -14,12 +14,13 @@
 	following = null
 
 /mob/observer/proc/start_following(var/atom/a)
-	stop_following()
-	following = a
-	GLOB.destroyed_event.register(a, src, .proc/stop_following)
-	GLOB.moved_event.register(a, src, .proc/keep_following)
-	GLOB.dir_set_event.register(a, src, /atom/proc/recursive_dir_set)
-	keep_following(new_loc = get_turf(following))
+	if(!istype(a, /obj/screen))
+		stop_following()
+		following = a
+		GLOB.destroyed_event.register(a, src, .proc/stop_following)
+		GLOB.moved_event.register(a, src, .proc/keep_following)
+		GLOB.dir_set_event.register(a, src, /atom/proc/recursive_dir_set)
+		keep_following(new_loc = get_turf(following))
 
 /mob/observer/proc/keep_following(var/atom/movable/moving_instance, var/atom/old_loc, var/atom/new_loc)
-	forceMove(get_turf(new_loc))
\ No newline at end of file
+	forceMove(get_turf(new_loc))
diff --git a/code/modules/mob/observer/ghost/ghost.dm b/code/modules/mob/observer/ghost/ghost.dm
index 5920ee86e5d..fd53203f261 100644
--- a/code/modules/mob/observer/ghost/ghost.dm
+++ b/code/modules/mob/observer/ghost/ghost.dm
@@ -32,6 +32,8 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images
 	var/obj/item/device/multitool/ghost_multitool
 	var/list/hud_images // A list of hud images
 
+	var/thearea
+
 /mob/observer/ghost/New(mob/body)
 	see_in_dark = 100
 	verbs += /mob/proc/toggle_antag_pool
@@ -71,7 +73,6 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images
 	ghost_multitool = new(src)
 
 	GLOB.ghost_mob_list += src
-
 	..()
 
 /mob/observer/ghost/Destroy()
@@ -108,7 +109,6 @@ Works together with spawning an observer, noted above.
 	..()
 	if(!loc) return
 	if(!client) return 0
-
 	handle_hud_glasses()
 
 	if(antagHUD)
@@ -150,6 +150,7 @@ Works together with spawning an observer, noted above.
 		ghost.key = key
 		if(ghost.client && !ghost.client.holder && !config.antag_hud_allowed)		// For new ghosts we remove the verb from even showing up if it's not allowed.
 			ghost.verbs -= /mob/observer/ghost/verb/toggle_antagHUD	// Poor guys, don't know what they are missing!
+		ghost.add_ghost_buttons()
 		return ghost
 
 /mob/observer/ghostize() // Do not create ghosts of ghosts.
@@ -233,6 +234,16 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
 		announce_ghost_joinleave(mind, 0, "They now occupy their body again.")
 	return 1
 
+/mob/observer/ghost/proc/jumptomob()
+	var/mob/M = input(usr, "Pick a mob", "Pick a mob") as null|anything in SSmobs.mob_list
+	log_and_message_admins("jumped to [key_name(M)]")
+	var/turf/T = get_turf(M)
+	if(T && isturf(T))
+		jumpTo(T)
+	else
+		to_chat(usr, "This mob is not located in the game world.")
+
+
 /mob/observer/ghost/verb/toggle_medHUD()
 	set category = "Ghost"
 	set name = "Toggle MedicHUD"
@@ -273,16 +284,17 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
 		M.antagHUD = 1
 		to_chat(src, SPAN_NOTICE("AntagHUD Enabled"))
 
-/mob/observer/ghost/verb/dead_tele(A in area_repository.get_areas_by_z_level())
+/mob/observer/ghost/verb/dead_tele()
 	set category = "Ghost"
 	set name = "Teleport"
 	set desc= "Teleport to a location"
 
+	var/A = input(usr, "Pick an area.", "Pick an area") as num|anything in area_repository.get_areas_by_z_level()
 	var/area/thearea = area_repository.get_areas_by_z_level()[A]
+
 	if(!thearea)
 		to_chat(src, "No area available.")
 		return
-
 	var/list/area_turfs = get_area_turfs(thearea, shall_check_if_holy() ? list(/proc/is_not_holy_turf) : list())
 	if(!area_turfs.len)
 		to_chat(src, "<span class='warning'>This area has been entirely made into sacred grounds, you cannot enter it while you are in this plane of existence!</span>")
@@ -300,13 +312,13 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
 		ghost_to_turf(T)
 	else
 		to_chat(src, "<span class='warning'>Invalid coordinates.</span>")
-/mob/observer/ghost/verb/follow(var/datum/follow_holder/fh in get_follow_targets())
+
+/mob/observer/ghost/verb/follow()
 	set category = "Ghost"
 	set name = "Follow"
 	set desc = "Follow and haunt a mob."
 
-	if(!fh.show_entry()) return
-	start_following(fh.followed_instance)
+	GLOB.orbit_menu.show(src)
 
 /mob/observer/ghost/proc/ghost_to_turf(var/turf/target_turf)
 	if(check_is_holy_turf(target_turf))
@@ -326,7 +338,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
 		verbs -= /mob/observer/ghost/verb/scan_target
 	..()
 
-/mob/observer/ghost/keep_following(var/atom/movable/am, var/old_loc, var/new_loc)
+/mob/observer/ghost/keep_following(var/obj/am, var/old_loc, var/new_loc)
 	var/turf/T = get_turf(new_loc)
 	if(check_is_holy_turf(T))
 		to_chat(src, "<span class='warning'>You cannot follow something standing on holy grounds!</span>")
@@ -623,3 +635,15 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
 		M.respawned_time = world.time
 	M.key = key
 	log_and_message_admins("has respawned.", M)
+
+/mob/observer/ghost/proc/add_ghost_buttons()
+	var/jumptomob = new /obj/screen/ghost/jumptomob()
+	var/orbit = new /obj/screen/ghost/orbit()
+	var/reenter_corpse = new /obj/screen/ghost/reenter_corpse()
+	var/teleport = new /obj/screen/ghost/teleport()
+	var/toggle_darkness = new /obj/screen/ghost/toggle_darkness()
+	client.screen.Add(jumptomob)
+	client.screen.Add(orbit)
+	client.screen.Add(reenter_corpse)
+	client.screen.Add(teleport)
+	client.screen.Add(toggle_darkness)
diff --git a/code/modules/mob/observer/ghost/orbit.dm b/code/modules/mob/observer/ghost/orbit.dm
new file mode 100644
index 00000000000..0c211df84d6
--- /dev/null
+++ b/code/modules/mob/observer/ghost/orbit.dm
@@ -0,0 +1,75 @@
+GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new)
+
+/datum/orbit_menu
+
+/datum/orbit_menu/tgui_state(mob/user)
+	return GLOB.tgui_observer_state
+
+/datum/orbit_menu/tgui_interact(mob/user, datum/tgui/ui)
+	ui = SStgui.try_update_ui(user, src, ui)
+	if (!ui)
+		ui = new(user, src, "Orbit", "Orbit")
+		ui.open()
+
+/datum/orbit_menu/tgui_act(action, list/params)
+	. = ..()
+
+	if(.)
+		return
+
+	switch(action)
+		if("orbit")
+			var/datum/follow_holder/fh = locate(params["ref"]) in get_follow_targets()
+			var/atom/movable/a = fh.followed_instance
+			var/mob/observer/ghost/G = usr
+			if(a != usr)
+				G.start_following(a)
+			return TRUE
+		if("refresh")
+			update_tgui_static_data()
+			return TRUE
+
+/datum/orbit_menu/tgui_static_data(mob/user)
+	var/list/data = list()
+	data["misc"] = list()
+	data["ghosts"] = list()
+	data["dead"] = list()
+	data["npcs"] = list()
+	data["alive"] = list()
+	data["antagonists"] = list()
+	for(var/datum/follow_holder/fh in get_follow_targets())
+		var/atom/movable/fi = fh.followed_instance
+		var/list/serialized = list()
+		serialized["name"] = fi.name
+		serialized["ref"] = "\ref[fh]"
+
+		if(!istype(fi, /mob))
+			data["misc"] += list(serialized)
+			continue
+		var/mob/M = fi
+		if(isobserver(M))
+			data["ghosts"] += list(serialized)
+			continue
+
+		if(M.stat == DEAD)
+			data["dead"] += list(serialized)
+			continue
+
+		if(M.mind == null)
+			data["npcs"] += list(serialized)
+			continue
+
+		data["alive"] += list(serialized)
+
+		var/mob/observer/ghost/O = user
+		if(O.antagHUD && M.get_antag_info())
+			var/antag_serialized = serialized.Copy()
+			for(var/antag_category in M.get_antag_info())
+				antag_serialized["antag"] += list(antag_category)
+			data["antagonists"] += list(antag_serialized)
+
+	return data
+
+/// Shows the UI to the specified user.
+/datum/orbit_menu/proc/show(mob/user)
+	tgui_interact(user)
diff --git a/icons/hud/screen_ghost.dmi b/icons/hud/screen_ghost.dmi
new file mode 100644
index 00000000000..52d72c731fc
Binary files /dev/null and b/icons/hud/screen_ghost.dmi differ
diff --git a/tgui/packages/tgui/interfaces/Orbit.js b/tgui/packages/tgui/interfaces/Orbit.js
new file mode 100644
index 00000000000..d4c4f603543
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/Orbit.js
@@ -0,0 +1,181 @@
+import { createSearch } from 'common/string';
+import { useBackend, useLocalState } from '../backend';
+import { Button, Divider, Flex, Icon, Input, Section } from '../components';
+import { Window } from '../layouts';
+
+const PATTERN_NUMBER = / \(([0-9]+)\)$/;
+
+const searchFor = (searchText) => createSearch(searchText, (thing) => thing.name);
+
+const compareString = (a, b) => (a < b ? -1 : a > b);
+
+const compareNumberedText = (a, b) => {
+  const aName = a.name;
+  const bName = b.name;
+
+  if (!aName || !bName) {
+    return 0;
+  }
+
+  // Check if aName and bName are the same except for a number at the end
+  // e.g. Medibot (2) and Medibot (3)
+  const aNumberMatch = aName.match(PATTERN_NUMBER);
+  const bNumberMatch = bName.match(PATTERN_NUMBER);
+
+  if (aNumberMatch && bNumberMatch && aName.replace(PATTERN_NUMBER, '') === bName.replace(PATTERN_NUMBER, '')) {
+    const aNumber = parseInt(aNumberMatch[1], 10);
+    const bNumber = parseInt(bNumberMatch[1], 10);
+
+    return aNumber - bNumber;
+  }
+
+  return compareString(aName, bName);
+};
+
+const BasicSection = (props, context) => {
+  const { act } = useBackend(context);
+  const { searchText, source, title } = props;
+  const things = source.filter(searchFor(searchText));
+  things.sort(compareNumberedText);
+  return (
+    source.length > 0 && (
+      <Section title={`${title} - (${source.length})`}>
+        {things.map((thing) => (
+          <Button
+            key={thing.name}
+            content={thing.name}
+            onClick={() =>
+              act('orbit', {
+                ref: thing.ref,
+              })
+            }
+          />
+        ))}
+      </Section>
+    )
+  );
+};
+
+const OrbitedButton = (props, context) => {
+  const { act } = useBackend(context);
+  const { color, thing } = props;
+
+  return (
+    <Button
+      color={color}
+      onClick={() =>
+        act('orbit', {
+          ref: thing.ref,
+        })
+      }>
+      {thing.name}
+    </Button>
+  );
+};
+
+export const Orbit = (props, context) => {
+  const { act, data } = useBackend(context);
+  const { alive, antagonists, dead, ghosts, misc, npcs } = data;
+
+  const [searchText, setSearchText] = useLocalState(context, 'searchText', '');
+
+  const collatedAntagonists = {};
+  for (const antagonist of antagonists) {
+    for (const anta of antagonist.antag) {
+      if (collatedAntagonists[anta] === undefined) {
+        collatedAntagonists[anta] = [];
+      }
+      collatedAntagonists[anta].push(antagonist);
+    }
+  }
+
+  const sortedAntagonists = Object.entries(collatedAntagonists);
+  sortedAntagonists.sort((a, b) => {
+    return compareString(a[0], b[0]);
+  });
+
+  const orbitMostRelevant = (searchText) => {
+    for (const source of [sortedAntagonists.map(([_, antags]) => antags), alive, ghosts, dead, npcs, misc]) {
+      const member = source.filter(searchFor(searchText)).sort(compareNumberedText)[0];
+      if (member !== undefined) {
+        act('orbit', { ref: member.ref });
+        break;
+      }
+    }
+  };
+
+  return (
+    <Window width={800} height={600}>
+      <Window.Content scrollable>
+        <Section>
+          <Flex>
+            <Flex.Item>
+              <Icon name="search" mr={1} />
+            </Flex.Item>
+            <Flex.Item grow={1}>
+              <Input
+                placeholder="Search..."
+                autoFocus
+                fluid
+                value={searchText}
+                onInput={(_, value) => setSearchText(value)}
+                onEnter={(_, value) => orbitMostRelevant(value)}
+              />
+            </Flex.Item>
+            <Flex.Item>
+              <Divider vertical />
+            </Flex.Item>
+            <Flex.Item>
+              <Button
+                inline
+                color="transparent"
+                tooltip="Refresh"
+                tooltipPosition="bottom-start"
+                icon="sync-alt"
+                onClick={() => act('refresh')}
+              />
+            </Flex.Item>
+          </Flex>
+        </Section>
+        {antagonists.length > 0 && (
+          <Section title="Antagonists">
+            {sortedAntagonists.map(([name, antags]) => (
+              <Section key={name} title={name} level={2}>
+                {antags
+                  .filter(searchFor(searchText))
+                  .sort(compareNumberedText)
+                  .map((antag) => (
+                    <OrbitedButton key={antag.name} color="bad" thing={antag} />
+                  ))}
+              </Section>
+            ))}
+          </Section>
+        )}
+
+        <Section title={`Alive - (${alive.length})`}>
+          {alive
+            .filter(searchFor(searchText))
+            .sort(compareNumberedText)
+            .map((thing) => (
+              <OrbitedButton key={thing.name} color="good" thing={thing} />
+            ))}
+        </Section>
+
+        <Section title={`Ghosts - (${ghosts.length})`}>
+          {ghosts
+            .filter(searchFor(searchText))
+            .sort(compareNumberedText)
+            .map((thing) => (
+              <OrbitedButton key={thing.name} color="grey" thing={thing} />
+            ))}
+        </Section>
+
+        <BasicSection title="Dead" source={dead} searchText={searchText} />
+
+        <BasicSection title="NPCs" source={npcs} searchText={searchText} />
+
+        <BasicSection title="Misc" source={misc} searchText={searchText} />
+      </Window.Content>
+    </Window>
+  );
+};