diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd
index 984f35f0..f639c490 100644
--- a/docs/labwc-actions.5.scd
+++ b/docs/labwc-actions.5.scd
@@ -180,6 +180,45 @@ Actions are used in menus and keyboard/mouse bindings.
 	*wrap* [yes|no] Wrap around from last desktop to first, and vice
 	versa. Default yes.
 
+**
+	Add virtual output (headless backend).
+
+	For example, it can be used to overlay virtual output on real output, but with
+	a different resolution (this can be done with `wlr-randr` or `wdisplays`).
+	After that, virtual output can be selected for screen sharing (casting),
+	effectively sharing only the region of the screen.
+
+	It must be noted that overlaying virtual output and real output is not
+	endorsed or explicitely supported by wlroots. For example, after configuring
+	virtual output, real output must be reconfigured as well (for the overlay
+	configuration to work correctly). This is the example configuration:
+
+```
+
+  
+  
+
+
+  
+
+```
+
+	Note that the vertical resolution of "ScreenCasting" output is just 50px
+	smaller than "eDP-1" output to cut off bottom panel from screen sharing.
+
+	Virtual output is also useful for extending the desktop to (maybe mobile)
+	remote systems like tablets. E.g. simply adding a virtual output, attaching
+	wayvnc to it and running a VNC client on the remote system.
+
+	*output_name* The name of virtual output. Providing virtual output name is
+	beneficial for further automation. Default is "HEADLESS-X".
+
+**
+	Remove virtual output (headless backend).
+
+	*output_name* The name of virtual output. If not supplied, will remove the
+	last virtual output added.
+
 **
 	If used as the only action for a binding: clear an earlier defined binding.
 
diff --git a/include/labwc.h b/include/labwc.h
index 86198a03..a4729178 100644
--- a/include/labwc.h
+++ b/include/labwc.h
@@ -203,6 +203,10 @@ struct server {
 	struct wlr_renderer *renderer;
 	struct wlr_allocator *allocator;
 	struct wlr_backend *backend;
+	struct headless {
+		struct wlr_backend *backend;
+		char pending_output_name[4096];
+	} headless;
 	struct wlr_session *session;
 
 	struct wlr_xdg_shell *xdg_shell;
@@ -467,6 +471,8 @@ struct wlr_box output_usable_area_in_layout_coords(struct output *output);
 struct wlr_box output_usable_area_scaled(struct output *output);
 void handle_output_power_manager_set_mode(struct wl_listener *listener,
 	void *data);
+void output_add_virtual(struct server *server, const char *output_name);
+void output_remove_virtual(struct server *server, const char *output_name);
 
 void server_init(struct server *server);
 void server_start(struct server *server);
diff --git a/src/action.c b/src/action.c
index 3a97efe4..7762bc92 100644
--- a/src/action.c
+++ b/src/action.c
@@ -98,6 +98,8 @@ enum action_type {
 	ACTION_TYPE_FOCUS_OUTPUT,
 	ACTION_TYPE_IF,
 	ACTION_TYPE_FOR_EACH,
+	ACTION_TYPE_VIRTUAL_OUTPUT_ADD,
+	ACTION_TYPE_VIRTUAL_OUTPUT_REMOVE,
 };
 
 const char *action_names[] = {
@@ -142,6 +144,8 @@ const char *action_names[] = {
 	"FocusOutput",
 	"If",
 	"ForEach",
+	"VirtualOutputAdd",
+	"VirtualOutputRemove",
 	NULL
 };
 
@@ -363,6 +367,13 @@ action_arg_from_xml_node(struct action *action, const char *nodename, const char
 			goto cleanup;
 		}
 		break;
+	case ACTION_TYPE_VIRTUAL_OUTPUT_ADD:
+	case ACTION_TYPE_VIRTUAL_OUTPUT_REMOVE:
+		if (!strcmp(argument, "output_name")) {
+			action_arg_add_str(action, argument, content);
+			goto cleanup;
+		}
+		break;
 	}
 
 	wlr_log(WLR_ERROR, "Invalid argument for action %s: '%s'",
@@ -915,6 +926,20 @@ actions_run(struct view *activator, struct server *server,
 				wl_array_release(&views);
 			}
 			break;
+		case ACTION_TYPE_VIRTUAL_OUTPUT_ADD:
+			{
+				const char *output_name = action_get_str(action, "output_name",
+						NULL);
+				output_add_virtual(server, output_name);
+			}
+			break;
+		case ACTION_TYPE_VIRTUAL_OUTPUT_REMOVE:
+			{
+				const char *output_name = action_get_str(action, "output_name",
+						NULL);
+				output_remove_virtual(server, output_name);
+			}
+			break;
 		case ACTION_TYPE_INVALID:
 			wlr_log(WLR_ERROR, "Not executing unknown action");
 			break;
diff --git a/src/output.c b/src/output.c
index e9ed9800..5be82fb4 100644
--- a/src/output.c
+++ b/src/output.c
@@ -9,6 +9,8 @@
 #define _POSIX_C_SOURCE 200809L
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -172,6 +174,12 @@ new_output_notify(struct wl_listener *listener, void *data)
 	struct server *server = wl_container_of(listener, server, new_output);
 	struct wlr_output *wlr_output = data;
 
+	/* Name virtual output */
+	if (wlr_output_is_headless(wlr_output) && server->headless.pending_output_name[0] != '\0') {
+		wlr_output_set_name(wlr_output, server->headless.pending_output_name);
+		server->headless.pending_output_name[0] = '\0';
+	}
+
 	/*
 	 * We offer any display as available for lease, some apps like
 	 * gamescope, want to take ownership of a display when they can
@@ -179,7 +187,7 @@ new_output_notify(struct wl_listener *listener, void *data)
 	 * This is also useful for debugging the DRM parts of
 	 * another compositor.
 	 */
-	if (server->drm_lease_manager) {
+	if (server->drm_lease_manager && wlr_output_is_drm(wlr_output)) {
 		wlr_drm_lease_v1_manager_offer_output(
 			server->drm_lease_manager, wlr_output);
 	}
@@ -746,3 +754,56 @@ handle_output_power_manager_set_mode(struct wl_listener *listener, void *data)
 		break;
 	}
 }
+
+void
+output_add_virtual(struct server *server, const char *output_name)
+{
+	if (output_name) {
+		/* Prevent creating outputs with the same name */
+		struct output *output;
+		wl_list_for_each(output, &server->outputs, link) {
+			if (wlr_output_is_headless(output->wlr_output) &&
+					!strcmp(output->wlr_output->name, output_name)) {
+				wlr_log(WLR_DEBUG,
+					"refusing to create virtual output with duplicate name");
+				return;
+			}
+		}
+		strncpy(server->headless.pending_output_name, output_name,
+				sizeof(server->headless.pending_output_name));
+	} else {
+		server->headless.pending_output_name[0] = '\0';
+	}
+	/*
+	 * Setting it to (0, 0) here disallows changing resolution from tools like
+	 * wlr-randr (returns error)
+	 */
+	wlr_headless_add_output(server->headless.backend, 1920, 1080);
+}
+
+void
+output_remove_virtual(struct server *server, const char *output_name)
+{
+	struct output *output;
+	wl_list_for_each(output, &server->outputs, link) {
+		if (wlr_output_is_headless(output->wlr_output)) {
+			if (output_name) {
+				/*
+				 * Given virtual output name, find and destroy virtual output by
+				 * that name.
+				 */
+				if (!strcmp(output->wlr_output->name, output_name)) {
+					wlr_output_destroy(output->wlr_output);
+					return;
+				}
+			} else {
+				/*
+				 * When virtual output name was no supplied by user, simply
+				 * destroy the first virtual output found.
+				 */
+				wlr_output_destroy(output->wlr_output);
+				return;
+			}
+		}
+	}
+}
diff --git a/src/server.c b/src/server.c
index 0aa07a09..e7a58be9 100644
--- a/src/server.c
+++ b/src/server.c
@@ -3,6 +3,8 @@
 #include "config.h"
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -256,6 +258,22 @@ server_init(struct server *server)
 		exit(EXIT_FAILURE);
 	}
 
+	/* Create headless backend to enable adding virtual outputs later on */
+	server->headless.backend = wlr_headless_backend_create(server->wl_display);
+	if (!server->headless.backend) {
+		wlr_log(WLR_ERROR, "unable to create headless backend");
+		exit(EXIT_FAILURE);
+	}
+	wlr_multi_backend_add(server->backend, server->headless.backend);
+
+	/*
+	 * If we don't populate headless backend with a virtual output (that we
+	 * create and immediately destroy), then virtual outputs being added
+	 * later do not work properly when overlaid on real output. Content is
+	 * drawn on the virtual output, but not drawn on the real output.
+	 */
+	wlr_output_destroy(wlr_headless_add_output(server->headless.backend, 0, 0));
+
 	/*
 	 * Autocreates a renderer, either Pixman, GLES2 or Vulkan for us. The
 	 * user can also specify a renderer using the WLR_RENDERER env var.