Overview
RuneLite’s overlay system allows plugins to render custom UI elements on top of the game. Overlays can display information, highlight game objects, or provide interactive controls. The OverlayManager handles overlay lifecycle and rendering.
Overlay Architecture
Overlay Base Class
All overlays extend the Overlay abstract class:
@ Getter
@ Setter
public abstract class Overlay implements LayoutableRenderableEntity
{
public static final float PRIORITY_LOW = 0f ;
public static final float PRIORITY_DEFAULT = 0.25f ;
public static final float PRIORITY_MED = 0.5f ;
public static final float PRIORITY_HIGH = 0.75f ;
public static final float PRIORITY_HIGHEST = 1f ;
@ Nullable
private final Plugin plugin ;
private Point preferredLocation ;
private Dimension preferredSize ;
private OverlayPosition preferredPosition ;
private Rectangle bounds = new Rectangle ();
private OverlayPosition position = OverlayPosition . TOP_LEFT ;
private float priority = PRIORITY_DEFAULT;
private OverlayLayer layer = OverlayLayer . UNDER_WIDGETS ;
private final List < Integer > drawHooks = new ArrayList <>();
private final List < OverlayMenuEntry > menuEntries = new ArrayList <>();
private boolean resizable ;
private int minimumSize = 32 ;
private boolean resettable = true ;
private boolean dragTargetable ;
private boolean movable = true ;
private boolean snappable = true ;
protected Overlay (@ Nullable Plugin plugin )
{
this . plugin = plugin;
}
}
Creating an Overlay
Basic Overlay
Create a simple text overlay:
import net.runelite.client.ui.overlay.Overlay;
import net.runelite.client.ui.overlay.OverlayPosition;
import java.awt.Dimension;
import java.awt.Graphics2D;
import javax.inject.Inject;
public class ExampleOverlay extends Overlay
{
private final Client client ;
private final ExamplePlugin plugin ;
@ Inject
private ExampleOverlay ( Client client , ExamplePlugin plugin )
{
super (plugin);
this . client = client;
this . plugin = plugin;
setPosition ( OverlayPosition . TOP_LEFT );
}
@ Override
public Dimension render ( Graphics2D graphics )
{
String text = "Example Overlay" ;
graphics . drawString (text, 0 , graphics . getFontMetrics (). getHeight ());
return null ;
}
}
Registering Overlays
Register overlays in your plugin’s startUp() method:
public class ExamplePlugin extends Plugin
{
@ Inject
private OverlayManager overlayManager ;
@ Inject
private ExampleOverlay overlay ;
@ Override
protected void startUp ()
{
overlayManager . add (overlay);
}
@ Override
protected void shutDown ()
{
overlayManager . remove (overlay);
}
}
Always remove overlays in shutDown() to prevent memory leaks and rendering issues.
Overlay Positions
Overlays can be positioned in different screen locations:
public enum OverlayPosition
{
TOP_LEFT ,
TOP_CENTER ,
TOP_RIGHT ,
BOTTOM_LEFT ,
BOTTOM_CENTER ,
BOTTOM_RIGHT ,
DYNAMIC , // Positioned dynamically (e.g., above NPCs)
DETACHED , // Freely movable
TOOLTIP , // At mouse cursor
CANVAS_TOP_RIGHT ,
ABOVE_CHATBOX_RIGHT
}
Example:
setPosition ( OverlayPosition . TOP_LEFT ); // Top-left corner
setPosition ( OverlayPosition . DYNAMIC ); // Dynamic positioning
setPosition ( OverlayPosition . TOOLTIP ); // Follow mouse cursor
Users can move overlays by holding Alt and dragging them. Set setMovable(false) to prevent this.
Overlay Layers
Overlays render at different z-order layers:
public enum OverlayLayer
{
UNDER_WIDGETS , // Below game interfaces
ABOVE_WIDGETS , // Above game interfaces
ABOVE_SCENE , // After 3D scene, before widgets
ALWAYS_ON_TOP , // Above everything
MANUAL // Custom draw hooks
}
Example:
setLayer ( OverlayLayer . ABOVE_WIDGETS ); // Draw above game UI
setLayer ( OverlayLayer . ABOVE_SCENE ); // Draw over 3D scene
Overlay Priority
Control render order with priority:
setPriority ( Overlay . PRIORITY_HIGH ); // Render later (on top)
setPriority ( Overlay . PRIORITY_LOW ); // Render earlier (below)
setPriority ( 0.75f ); // Custom priority
For DYNAMIC overlays, higher priority renders later (on top). For positioned overlays, higher priority renders earlier (closer to preferred position).
OverlayPanel
Use OverlayPanel for styled panel overlays:
import net.runelite.client.ui.overlay.OverlayPanel;
import net.runelite.client.ui.overlay.components.TitleComponent;
import net.runelite.client.ui.overlay.components.LineComponent;
public class ExampleOverlayPanel extends OverlayPanel
{
private final ExamplePlugin plugin ;
@ Inject
private ExampleOverlayPanel ( ExamplePlugin plugin )
{
super (plugin);
this . plugin = plugin;
}
@ Override
public Dimension render ( Graphics2D graphics )
{
panelComponent . getChildren (). clear ();
// Add title
panelComponent . getChildren (). add ( TitleComponent . builder ()
. text ( "Example Panel" )
. color ( Color . GREEN )
. build ());
// Add line
panelComponent . getChildren (). add ( LineComponent . builder ()
. left ( "Status:" )
. right ( "Active" )
. build ());
return super . render (graphics);
}
}
OverlayPanel automatically handles background rendering, borders, and positioning. Use it for simple information panels.
Overlay Components
RuneLite provides pre-built components for common UI patterns:
TitleComponent
TitleComponent . builder ()
. text ( "My Title" )
. color ( Color . YELLOW )
. build ()
LineComponent
LineComponent . builder ()
. left ( "Label:" )
. right ( "Value" )
. leftColor ( Color . WHITE )
. rightColor ( Color . GREEN )
. build ()
ProgressBarComponent
ProgressBarComponent . builder ()
. minimum ( 0 )
. maximum ( 100 )
. value (currentValue)
. labelDisplayMode ( ProgressBarComponent . LabelDisplayMode . PERCENTAGE )
. build ()
TableComponent
TableComponent tableComponent = new TableComponent ();
tableComponent . setColumnAlignments (
TableAlignment . LEFT ,
TableAlignment . RIGHT
);
tableComponent . addRow ( "Item" , "Quantity" );
tableComponent . addRow ( "Gold" , "1000" );
ImageComponent
BufferedImage image = ImageUtil . loadImageResource ( getClass (), "icon.png" );
ImageComponent . builder ()
. image (image)
. build ()
Dynamic Overlays
Dynamic overlays render at specific world positions:
import net.runelite.api.Point;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.Perspective;
public class NPCOverlay extends Overlay
{
private final Client client ;
private final ExamplePlugin plugin ;
@ Inject
private NPCOverlay ( Client client , ExamplePlugin plugin )
{
super (plugin);
this . client = client;
this . plugin = plugin;
setPosition ( OverlayPosition . DYNAMIC );
setLayer ( OverlayLayer . ABOVE_SCENE );
}
@ Override
public Dimension render ( Graphics2D graphics )
{
for ( NPC npc : client . getNpcs ())
{
if ( npc . getName () != null && npc . getName (). equals ( "Goblin" ))
{
renderNpcOverlay (graphics, npc);
}
}
return null ;
}
private void renderNpcOverlay ( Graphics2D graphics , NPC npc )
{
Point canvasPoint = npc . getCanvasTextLocation (graphics, "Target" , 0 );
if (canvasPoint != null )
{
graphics . setColor ( Color . RED );
graphics . drawString ( "Target" , canvasPoint . getX (), canvasPoint . getY ());
}
// Draw tile overlay
LocalPoint localPoint = npc . getLocalLocation ();
if (localPoint != null )
{
Polygon poly = Perspective . getCanvasTilePoly (client, localPoint);
if (poly != null )
{
graphics . setColor ( new Color ( 255 , 0 , 0 , 100 ));
graphics . fill (poly);
graphics . setColor ( Color . RED );
graphics . draw (poly);
}
}
}
}
Overlay Manager
The OverlayManager provides overlay lifecycle management:
OverlayManager.java:57-114
@ Singleton
@ Slf4j
public class OverlayManager
{
private final List < Overlay > overlays = new ArrayList <>();
private Collection < WidgetItem > widgetItems = Collections . emptyList ();
private ArrayListMultimap < Object , Overlay > overlayMap = ArrayListMultimap . create ();
private final ConfigManager configManager ;
private final RuneLiteConfig runeLiteConfig ;
@ Inject
private OverlayManager ( final ConfigManager configManager , final RuneLiteConfig runeLiteConfig )
{
this . configManager = configManager;
this . runeLiteConfig = runeLiteConfig;
}
}
Adding Overlays
OverlayManager.java:168-189
public synchronized boolean add ( final Overlay overlay)
{
if ( overlays . contains (overlay)) {
return false ;
}
overlays . add (overlay);
loadOverlay (overlay);
updateOverlayConfig (overlay);
if (overlay instanceof WidgetItemOverlay) {
((WidgetItemOverlay) overlay). setOverlayManager ( this );
}
rebuildOverlayLayers ();
return true ;
}
Removing Overlays
OverlayManager.java:197-207
public synchronized boolean remove ( final Overlay overlay)
{
final boolean remove = overlays . remove (overlay);
if (remove) {
rebuildOverlayLayers ();
}
return remove;
}
Saving and Resetting
// Save overlay position and size
overlayManager . saveOverlay (overlay);
// Reset to defaults
overlayManager . resetOverlay (overlay);
Overlay Persistence
Overlay positions and sizes are automatically saved:
OverlayManager.java:252-258
public synchronized void saveOverlay ( final Overlay overlay)
{
saveOverlayPosition (overlay);
saveOverlaySize (overlay);
saveOverlayLocation (overlay);
rebuildOverlayLayers ();
}
Configuration keys:
runelite.ExampleOverlay_preferredLocation=100:200
runelite.ExampleOverlay_preferredSize=300x150
runelite.ExampleOverlay_preferredPosition=TOP_LEFT
Overlay configuration is stored per-profile and synced if cloud sync is enabled.
Add custom menu entries to overlays:
public class ExampleOverlay extends Overlay
{
@ Inject
private ExampleOverlay ( ExamplePlugin plugin )
{
super (plugin);
// Add custom menu entry
addMenuEntry (
MenuAction . RUNELITE_OVERLAY ,
"Toggle mode" ,
getName (),
e -> toggleMode ()
);
}
private void toggleMode ()
{
// Handle menu click
}
}
Conditional Rendering
Only render when needed:
@ Override
public Dimension render ( Graphics2D graphics)
{
// Don't render when not logged in
if ( client . getGameState () != GameState . LOGGED_IN )
{
return null ;
}
// Don't render if plugin disabled
if ( ! plugin . isEnabled ())
{
return null ;
}
// Render overlay
renderOverlay (graphics);
return null ;
}
Returning null from render() means the overlay has no fixed size. Return a Dimension for fixed-size overlays.
Create overlays that follow the mouse:
public class TooltipOverlay extends Overlay
{
@ Inject
private TooltipOverlay ( ExamplePlugin plugin )
{
super (plugin);
setPosition ( OverlayPosition . TOOLTIP );
setPriority ( Overlay . PRIORITY_HIGHEST );
}
@ Override
public Dimension render ( Graphics2D graphics )
{
String tooltipText = plugin . getTooltipText ();
if (tooltipText == null )
{
return null ;
}
FontMetrics fm = graphics . getFontMetrics ();
int width = fm . stringWidth (tooltipText) + 10 ;
int height = fm . getHeight () + 10 ;
// Draw background
graphics . setColor ( new Color ( 0 , 0 , 0 , 200 ));
graphics . fillRect ( 0 , 0 , width, height);
// Draw text
graphics . setColor ( Color . WHITE );
graphics . drawString (tooltipText, 5 , fm . getHeight ());
return new Dimension (width, height);
}
}
Highlight or render over inventory/equipment items:
import net.runelite.client.ui.overlay.WidgetItemOverlay;
import net.runelite.api.widgets.WidgetItem;
public class InventoryOverlay extends WidgetItemOverlay
{
private final ExamplePlugin plugin ;
@ Inject
private InventoryOverlay ( ExamplePlugin plugin )
{
this . plugin = plugin;
showOnInventory ();
showOnEquipment ();
}
@ Override
public void renderItemOverlay ( Graphics2D graphics , int itemId , WidgetItem widgetItem )
{
if ( plugin . shouldHighlight (itemId))
{
Rectangle bounds = widgetItem . getCanvasBounds ();
graphics . setColor ( new Color ( 0 , 255 , 0 , 100 ));
graphics . fill (bounds);
}
}
}
Custom Draw Hooks
Render after specific interfaces or layers:
import net.runelite.api.widgets.WidgetID;
public class CustomOverlay extends Overlay
{
@ Inject
private CustomOverlay ( ExamplePlugin plugin )
{
super (plugin);
setLayer ( OverlayLayer . MANUAL );
// Draw after chatbox interface
drawAfterInterface ( WidgetID . CHATBOX_GROUP_ID );
// Draw after specific layer
drawAfterLayer ( WidgetID . BANK_GROUP_ID , 0 );
}
}
When using MANUAL layer with draw hooks, the overlay won’t render during the normal layer passes. It only renders at the specified hook points.
Advanced Graphics
Anti-aliasing
graphics . setRenderingHint ( RenderingHints . KEY_ANTIALIASING ,
RenderingHints . VALUE_ANTIALIAS_ON );
Transparency
Composite originalComposite = graphics . getComposite ();
graphics . setComposite ( AlphaComposite . getInstance ( AlphaComposite . SRC_OVER , 0.5f ));
// Draw transparent elements
graphics . setComposite (originalComposite);
Clipping
Shape originalClip = graphics . getClip ();
graphics . setClip ( new Rectangle (x, y, width, height));
// Draw clipped content
graphics . setClip (originalClip);
Best Practices
The render() method is called every frame (~50 times per second). Avoid expensive operations: // Bad: Expensive calculation every frame
public Dimension render ( Graphics2D graphics) {
List < NPC > targets = findAllTargets (); // Slow!
// ...
}
// Good: Cache results
private List < NPC > cachedTargets = new ArrayList <>();
@ Subscribe
public void onGameTick ( GameTick event) {
cachedTargets = findAllTargets ();
}
public Dimension render ( Graphics2D graphics) {
for ( NPC npc : cachedTargets) {
// ...
}
}
Always verify the game state before rendering: if ( client . getGameState () != GameState . LOGGED_IN ) {
return null ;
}
Remove overlays in shutDown() and clean up any graphics resources: @ Override
protected void shutDown () {
overlayManager . remove (overlay);
// Clean up images, etc.
}
Choose the correct layer for your overlay:
ABOVE_SCENE for world overlays
UNDER_WIDGETS for background info
ABOVE_WIDGETS for prominent displays
Game state can be incomplete. Always null-check: Player player = client . getLocalPlayer ();
if (player == null ) {
return null ;
}
String name = player . getName ();
if (name == null ) {
return null ;
}
Cache Expensive Calculations
Calculate complex values on game ticks, not every frame
Limit Text Rendering
Text rendering is expensive. Pre-calculate dimensions when possible
Use Dirty Flags
Only recompute when data changes: private boolean dirty = true ;
private String cachedText ;
@ Subscribe
public void onConfigChanged ( ConfigChanged event) {
dirty = true ;
}
public Dimension render ( Graphics2D graphics) {
if (dirty) {
cachedText = computeText ();
dirty = false ;
}
graphics . drawString (cachedText, 0 , 10 );
}
Avoid Creating Objects
Don’t create new objects every frame - reuse them
Next Steps
Plugin Examples See real plugin examples with overlays
Creating Overlays Build custom overlays in your plugins
Perspective API Convert world coordinates to screen space
Creating Overlays Create custom overlays in your plugins