diff --git a/Content.Client/MapText/MapTextComponent.cs b/Content.Client/MapText/MapTextComponent.cs
new file mode 100644
index 0000000000..f70b3c8dd4
--- /dev/null
+++ b/Content.Client/MapText/MapTextComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.MapText;
+using Robust.Client.Graphics;
+
+namespace Content.Client.MapText;
+
+[RegisterComponent]
+public sealed partial class MapTextComponent : SharedMapTextComponent
+{
+ ///
+ /// The font that gets cached on component init or state changes
+ ///
+ [ViewVariables]
+ public VectorFont? CachedFont;
+
+ ///
+ /// The text currently being displayed. This is either or the
+ /// localized text or
+ ///
+ public string CachedText = string.Empty;
+}
diff --git a/Content.Client/MapText/MapTextOverlay.cs b/Content.Client/MapText/MapTextOverlay.cs
new file mode 100644
index 0000000000..70e708bba8
--- /dev/null
+++ b/Content.Client/MapText/MapTextOverlay.cs
@@ -0,0 +1,85 @@
+using System.Numerics;
+using Content.Shared.MapText;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.MapText;
+
+///
+/// Draws map text as an overlay
+///
+public sealed class MapTextOverlay : Overlay
+{
+ private readonly IConfigurationManager _configManager;
+ private readonly IEntityManager _entManager;
+ private readonly IUserInterfaceManager _uiManager;
+ private readonly SharedTransformSystem _transform;
+ public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+
+ public MapTextOverlay(
+ IConfigurationManager configManager,
+ IEntityManager entManager,
+ IUserInterfaceManager uiManager,
+ SharedTransformSystem transform,
+ IResourceCache resourceCache,
+ IPrototypeManager prototypeManager)
+ {
+ _configManager = configManager;
+ _entManager = entManager;
+ _uiManager = uiManager;
+ _transform = transform;
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (args.ViewportControl == null)
+ return;
+
+ args.DrawingHandle.SetTransform(Matrix3x2.Identity);
+
+ var scale = _configManager.GetCVar(CVars.DisplayUIScale);
+
+ if (scale == 0f)
+ scale = _uiManager.DefaultUIScale;
+
+ DrawWorld(args.ScreenHandle, args, scale);
+
+ args.DrawingHandle.UseShader(null);
+ }
+
+ private void DrawWorld(DrawingHandleScreen handle, OverlayDrawArgs args, float scale)
+ {
+ if ( args.ViewportControl == null)
+ return;
+
+ var matrix = args.ViewportControl.GetWorldToScreenMatrix();
+ var query = _entManager.AllEntityQueryEnumerator();
+
+ // Enlarge bounds to try prevent pop-in due to large text.
+ var bounds = args.WorldBounds.Enlarged(2);
+
+ while(query.MoveNext(out var uid, out var mapText))
+ {
+ var mapPos = _transform.GetMapCoordinates(uid);
+
+ if (mapPos.MapId != args.MapId)
+ continue;
+
+ if (!bounds.Contains(mapPos.Position))
+ continue;
+
+ if (mapText.CachedFont == null)
+ continue;
+
+ var pos = Vector2.Transform(mapPos.Position, matrix) + mapText.Offset;
+ var dimensions = handle.GetDimensions(mapText.CachedFont, mapText.CachedText, scale);
+ handle.DrawString(mapText.CachedFont, pos - dimensions / 2f, mapText.CachedText, scale, mapText.Color);
+ }
+ }
+}
diff --git a/Content.Client/MapText/MapTextSystem.cs b/Content.Client/MapText/MapTextSystem.cs
new file mode 100644
index 0000000000..96ce8f93c2
--- /dev/null
+++ b/Content.Client/MapText/MapTextSystem.cs
@@ -0,0 +1,81 @@
+using Content.Shared.MapText;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.MapText;
+
+///
+public sealed class MapTextSystem : SharedMapTextSystem
+{
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
+
+ private MapTextOverlay _overlay = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnComponentStartup);
+ SubscribeLocalEvent(HandleCompState);
+
+ _overlay = new MapTextOverlay(_configManager, EntityManager, _uiManager, _transform, _resourceCache, _prototypeManager);
+ _overlayManager.AddOverlay(_overlay);
+
+ // TODO move font prototype to robust.shared, then use ProtoId
+ DebugTools.Assert(_prototypeManager.HasIndex(SharedMapTextComponent.DefaultFont));
+ }
+
+ private void OnComponentStartup(Entity ent, ref ComponentStartup args)
+ {
+ CacheText(ent.Comp);
+ // TODO move font prototype to robust.shared, then use ProtoId
+ DebugTools.Assert(_prototypeManager.HasIndex(ent.Comp.FontId));
+ }
+
+ private void HandleCompState(Entity ent, ref ComponentHandleState args)
+ {
+ if (args.Current is not MapTextComponentState state)
+ return;
+
+ ent.Comp.Text = state.Text;
+ ent.Comp.LocText = state.LocText;
+ ent.Comp.Color = state.Color;
+ ent.Comp.FontId = state.FontId;
+ ent.Comp.FontSize = state.FontSize;
+ ent.Comp.Offset = state.Offset;
+
+ CacheText(ent.Comp);
+ }
+
+ private void CacheText(MapTextComponent component)
+ {
+ component.CachedFont = null;
+
+ component.CachedText = string.IsNullOrWhiteSpace(component.Text)
+ ? Loc.GetString(component.LocText)
+ : component.Text;
+
+ if (!_prototypeManager.TryIndex(component.FontId, out var fontPrototype))
+ {
+ component.CachedText = Loc.GetString("map-text-font-error");
+ component.Color = Color.Red;
+
+ if(_prototypeManager.TryIndex(SharedMapTextComponent.DefaultFont, out var @default))
+ component.CachedFont = new VectorFont(_resourceCache.GetResource(@default.Path), 14);
+ return;
+ }
+
+ var fontResource = _resourceCache.GetResource(fontPrototype.Path);
+ component.CachedFont = new VectorFont(fontResource, component.FontSize);
+ }
+}
diff --git a/Content.Server/MapText/MapTextComponent.cs b/Content.Server/MapText/MapTextComponent.cs
new file mode 100644
index 0000000000..2af9583484
--- /dev/null
+++ b/Content.Server/MapText/MapTextComponent.cs
@@ -0,0 +1,6 @@
+using Content.Shared.MapText;
+
+namespace Content.Server.MapText;
+
+[RegisterComponent]
+public sealed partial class MapTextComponent : SharedMapTextComponent;
diff --git a/Content.Server/MapText/MapTextSystem.cs b/Content.Server/MapText/MapTextSystem.cs
new file mode 100644
index 0000000000..5632c06e1a
--- /dev/null
+++ b/Content.Server/MapText/MapTextSystem.cs
@@ -0,0 +1,28 @@
+using Content.Shared.MapText;
+using Robust.Shared.GameStates;
+
+namespace Content.Server.MapText;
+
+///
+public sealed class MapTextSystem : SharedMapTextSystem
+{
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(GetCompState);
+ }
+
+ private void GetCompState(Entity ent, ref ComponentGetState args)
+ {
+ args.State = new MapTextComponentState
+ {
+ Text = ent.Comp.Text,
+ LocText = ent.Comp.LocText,
+ Color = ent.Comp.Color,
+ FontId = ent.Comp.FontId,
+ FontSize = ent.Comp.FontSize,
+ Offset = ent.Comp.Offset
+ };
+ }
+}
diff --git a/Content.Shared/MapText/SharedMapTextComponent.cs b/Content.Shared/MapText/SharedMapTextComponent.cs
new file mode 100644
index 0000000000..7b8a02da6f
--- /dev/null
+++ b/Content.Shared/MapText/SharedMapTextComponent.cs
@@ -0,0 +1,51 @@
+using System.Numerics;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.MapText;
+
+///
+/// This is used for displaying text in world space
+///
+
+[NetworkedComponent, Access(typeof(SharedMapTextSystem))]
+public abstract partial class SharedMapTextComponent : Component
+{
+ public const string DefaultFont = "Default";
+
+ ///
+ /// The text to display. This will override .
+ ///
+ [DataField]
+ public string? Text;
+
+ ///
+ /// The localized-id of the text that should be displayed.
+ ///
+ [DataField]
+ public LocId LocText = "map-text-default";
+ // TODO VV: LocId editing
+
+ [DataField]
+ public Color Color = Color.White;
+
+ [DataField]
+ public string FontId = DefaultFont;
+
+ [DataField]
+ public int FontSize = 12;
+
+ [DataField]
+ public Vector2 Offset = Vector2.Zero;
+}
+
+[Serializable, NetSerializable]
+public sealed class MapTextComponentState : ComponentState
+{
+ public string? Text { get; init;}
+ public LocId LocText { get; init;}
+ public Color Color { get; init;}
+ public string FontId { get; init; } = default!;
+ public int FontSize { get; init;}
+ public Vector2 Offset { get; init;}
+}
diff --git a/Content.Shared/MapText/SharedMapTextSystem.cs b/Content.Shared/MapText/SharedMapTextSystem.cs
new file mode 100644
index 0000000000..da2adf16cf
--- /dev/null
+++ b/Content.Shared/MapText/SharedMapTextSystem.cs
@@ -0,0 +1,6 @@
+namespace Content.Shared.MapText;
+
+///
+/// This handles registering the map text overlay, caching the text font and handling component state
+///
+public abstract class SharedMapTextSystem : EntitySystem;
diff --git a/Resources/Locale/en-US/mapping/map-text-component.ftl b/Resources/Locale/en-US/mapping/map-text-component.ftl
new file mode 100644
index 0000000000..0a4d54b485
--- /dev/null
+++ b/Resources/Locale/en-US/mapping/map-text-component.ftl
@@ -0,0 +1,2 @@
+map-text-default = Use VV to change the displayed text
+map-text-font-error = "Error - invalid font"
diff --git a/Resources/Prototypes/Entities/Markers/map_text.yml b/Resources/Prototypes/Entities/Markers/map_text.yml
new file mode 100644
index 0000000000..e8496138ac
--- /dev/null
+++ b/Resources/Prototypes/Entities/Markers/map_text.yml
@@ -0,0 +1,11 @@
+- type: entity
+ id: MapText
+ parent: MarkerBase
+ name: map text
+ placement:
+ mode: PlaceFree
+ components:
+ - type: MapText
+ - type: Sprite
+ state: pink
+