diff --git a/ui/context.go b/ui/context.go
index 1d916fe..0bef705 100644
--- a/ui/context.go
+++ b/ui/context.go
@@ -47,6 +47,7 @@ func newContext(r Renderer, s *Style, view Control) *context {
 		fonts:    NewFonts(r),
 		textures: NewTextures(r)}
 	ctx.overlays.AddOnTop(uiDefaultTooltipOverlay, ctx.tooltip, false)
+	ctx.overlays.AddOnTop(DefaultDebugOverlay, NewDebugOverlay(view), false)
 	return ctx
 }
 
diff --git a/ui/debug.go b/ui/debug.go
new file mode 100644
index 0000000..1fb007c
--- /dev/null
+++ b/ui/debug.go
@@ -0,0 +1,149 @@
+package ui
+
+import (
+	"image/color"
+	"reflect"
+
+	"opslag.de/schobers/geom"
+
+	"opslag.de/schobers/zntg"
+)
+
+const DefaultDebugOverlay = "ui-default-debug"
+
+func controlChildren(control Control) []Control {
+	container, ok := control.(*ContainerBase)
+	if ok {
+		return container.Children
+	}
+
+	proxy, ok := control.(*Proxy)
+	if ok {
+		return []Control{proxy.Content}
+	}
+	return controlChildrenReflect(reflect.ValueOf(control))
+}
+
+func controlChildrenReflect(control reflect.Value) []Control {
+	switch control.Kind() {
+	case reflect.Interface:
+		return controlChildrenReflect(control.Elem())
+	case reflect.Ptr:
+		return controlChildrenReflect(control.Elem())
+	}
+	if reflect.TypeOf(ContainerBase{}) == control.Type() {
+		container := control.Interface().(ContainerBase)
+		return container.Children
+	}
+	if reflect.TypeOf(Proxy{}) == control.Type() {
+		proxy := control.Interface().(Proxy)
+		return []Control{proxy.Content}
+	}
+	if control.NumField() == 0 {
+		return nil
+	}
+	field := control.Type().Field(0)
+	if !field.Anonymous {
+		return nil
+	}
+	return controlChildrenReflect(control.Field(0))
+}
+
+func controlName(control Control) string {
+	typ := reflect.TypeOf(control)
+	return typ.Elem().Name()
+}
+
+type debugOverlay struct {
+	ControlBase
+
+	root Control
+
+	hoverNodes *controlNode
+
+	boundsColor     color.Color
+	textColor       color.Color
+	textShadowColor color.Color
+}
+
+func NewDebugOverlay(root Control) *debugOverlay {
+	return &debugOverlay{
+		root:            root,
+		boundsColor:     zntg.MustHexColor(`#00FF003F`),
+		textColor:       zntg.MustHexColor(`#FFFFFF3F`),
+		textShadowColor: zntg.MustHexColor(`#0000003F`),
+	}
+}
+
+func (o *debugOverlay) renderControl(ctx Context, control Control) {
+	renderer := ctx.Renderer()
+	// bounds := control.Bounds()
+	// renderer.Rectangle(bounds, o.boundsColor, 1)
+
+	var maxY float32
+	var renderHoverNode func(pos geom.PointF32, node *controlNode)
+	renderHoverNode = func(pos geom.PointF32, node *controlNode) {
+		if node == nil {
+			return
+		}
+		nameTexture, err := ctx.Fonts().TextTexture("debug", color.White, node.Name)
+		if err != nil {
+			return
+		}
+		defer nameTexture.Destroy()
+		renderer.FillRectangle(pos.RectRel2D(nameTexture.Width(), nameTexture.Height()), color.Black)
+		renderer.DrawTexturePoint(nameTexture, pos)
+		childPos := pos.Add2D(nameTexture.Width()+8, 0)
+		for _, child := range node.Children {
+			if childPos.Y == maxY {
+				childPos.Y = maxY + nameTexture.Height()
+			}
+			renderHoverNode(childPos, child)
+			maxY = childPos.Y
+			childPos.Y += nameTexture.Height() + 8
+		}
+	}
+	renderHoverNode(geom.PtF32(4, 4), o.hoverNodes)
+
+	children := controlChildren(control)
+	for _, child := range children {
+		o.renderControl(ctx, child)
+	}
+}
+
+func createHoverNodes(hover geom.PointF32, control Control) *controlNode {
+	if !hover.In(control.Bounds()) {
+		return nil
+	}
+	node := &controlNode{Name: controlName(control)}
+	for _, child := range controlChildren(control) {
+		childNode := createHoverNodes(hover, child)
+		if childNode != nil {
+			node.Children = append(node.Children, childNode)
+		}
+	}
+	return node
+}
+
+func (o *debugOverlay) Handle(ctx Context, e Event) bool {
+	switch e := e.(type) {
+	case *MouseMoveEvent:
+		o.hoverNodes = createHoverNodes(e.Pos(), o.root)
+	case *MouseLeaveEvent:
+		o.hoverNodes = nil
+	}
+	return false
+}
+
+func (o *debugOverlay) Hidden() {}
+
+func (o *debugOverlay) Render(ctx Context) {
+	o.renderControl(ctx, o.root)
+}
+
+func (o *debugOverlay) Shown() {}
+
+type controlNode struct {
+	Name     string
+	Children []*controlNode
+}
diff --git a/ui/debug_test.go b/ui/debug_test.go
new file mode 100644
index 0000000..b0ac196
--- /dev/null
+++ b/ui/debug_test.go
@@ -0,0 +1,21 @@
+package ui
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestControlName(t *testing.T) {
+	assert.Equal(t, "ControlBase", controlName(&ControlBase{}))
+	assert.Equal(t, "Label", controlName(&Label{}))
+}
+
+func TestControlChildren(t *testing.T) {
+	assert.Len(t, controlChildren(&ContainerBase{}), 0)
+	assert.Len(t, controlChildren(&ControlBase{}), 0)
+	assert.Len(t, controlChildren(&ContainerBase{Children: []Control{nil, nil}}), 2)
+	assert.Len(t, controlChildren(&StackPanel{ContainerBase: ContainerBase{Children: []Control{nil, nil}}}), 2)
+	assert.Len(t, controlChildren(&Proxy{Content: &ControlBase{}}), 1)
+	assert.Len(t, controlChildren(&overflow{Proxy: Proxy{Content: &ControlBase{}}}), 1)
+}