diff --git a/ivette/src/dome/renderer/errors.tsx b/ivette/src/dome/renderer/errors.tsx
index abf7e91bc6d2732d8975538d86db9877e4600ad5..9635e2625d646e286b94d603edcd3b63d6055826 100644
--- a/ivette/src/dome/renderer/errors.tsx
+++ b/ivette/src/dome/renderer/errors.tsx
@@ -30,7 +30,7 @@
 */
 
 import React, { ReactNode } from 'react';
-import { Debug } from 'dome';
+import { DEVEL, Debug } from 'dome';
 import { Label } from 'dome/controls/labels';
 import { Button } from 'dome/controls/buttons';
 
@@ -53,7 +53,6 @@ export interface CatchProps {
   label?: string;
   /** Alternative renderer callback in case of errors. */
   onError?: JSX.Element | ErrorRenderer;
-
   children: ReactNode;
 }
 
@@ -62,25 +61,34 @@ interface CatchState {
   info?: unknown;
 }
 
+/* eslint-disable react/prop-types */
+
 /**
    React Error Boundaries.
  */
 export class Catch extends React.Component<CatchProps, CatchState, unknown> {
 
-  constructor(private p: CatchProps) {
-    super(p);
+  constructor(props: CatchProps) {
+    super(props);
     this.state = {};
     this.logerr = this.logerr.bind(this);
     this.reload = this.reload.bind(this);
   }
 
   componentDidCatch(error: unknown, info: unknown): void {
-    this.setState({ error, info });
+    if (DEVEL) {
+      const { label='Error' } = this.props;
+      D.error(label, ': ', error, info);
+    }
+  }
+
+  static getDerivedStateFromError(error: unknown, info: unknown): CatchState {
+    return { error, info };
   }
 
   logerr(): void {
     const { error, info } = this.state;
-    D.error('catched error:', error, info);
+    D.error('Catched error:', error, info);
   }
 
   reload(): void {
@@ -89,7 +97,7 @@ export class Catch extends React.Component<CatchProps, CatchState, unknown> {
 
   render(): JSX.Element {
     const { error, info } = this.state;
-    const { onError, label = 'Error' } = this.p;
+    const { onError, label = 'Error' } = this.props;
     if (error) {
       if (typeof onError === 'function')
         return onError(error, info, this.reload);
@@ -106,7 +114,7 @@ export class Catch extends React.Component<CatchProps, CatchState, unknown> {
         </div>
       );
     }
-    return (<>{this.p.children}</>);
+    return (<>{this.props.children}</>);
   }
 }
 
diff --git a/ivette/src/dome/renderer/graph/diagram.tsx b/ivette/src/dome/renderer/graph/diagram.tsx
index 5f111e7322f7893d5d168f5a3e5dd7ff3ceb388f..f2c2c732aff6a8b1d98bca85ed4c2dcc589fc3b3 100644
--- a/ivette/src/dome/renderer/graph/diagram.tsx
+++ b/ivette/src/dome/renderer/graph/diagram.tsx
@@ -58,6 +58,8 @@ export type Box = Cell | Box[];
 export interface Node {
   /** Node identifier (unique). */
   id: string;
+  /** Cluster identifier */
+  cluster?: string;
   /** Node label */
   label?: string;
   /** Node tooltip */
@@ -102,13 +104,25 @@ export interface Edge {
   tailLabel?: string,
 }
 
+export interface Cluster {
+  /** Identifier */
+  id: string;
+  /** Label (default is none) */
+  label?: string;
+  /** Title (default is none) */
+  title?: string;
+  /** Background color (default is grey) */
+  color?: Color;
+}
+
 /* -------------------------------------------------------------------------- */
 /* --- Graph Component Properties                                         --- */
 /* -------------------------------------------------------------------------- */
 
 export interface DiagramProps {
-  nodes: readonly Node[];
-  edges: readonly Edge[];
+  nodes?: readonly Node[];
+  edges?: readonly Edge[];
+  clusters?: readonly Cluster[];
 
   /**
      Element to focus on.
@@ -137,6 +151,7 @@ export interface DiagramProps {
 /* --- Color Model                                                        --- */
 /* -------------------------------------------------------------------------- */
 
+// node background colors
 const BGCOLOR = {
   'white': '#fff',
   'grey': '#ccc',
@@ -151,6 +166,22 @@ const BGCOLOR = {
   'pink': 'hotpink',
 };
 
+// cluster background colors
+const SGCOLOR = {
+  'white': '#eee',
+  'grey': '#ccc',
+  'dark': '#aaa',
+  'primary': '#4fc3f7',
+  'selected': '#90caf9',
+  'green': '#AED581',
+  'orange': '#FFCC80',
+  'red': '#ff6e6e',
+  'yellow': '#fff59d',
+  'blue': '#bbdefb',
+  'pink': '#f8bbd0',
+};
+
+// foreground colors
 const FGCOLOR = {
   'white': 'black',
   'grey': 'black',
@@ -165,6 +196,7 @@ const FGCOLOR = {
   'pink': 'white',
 };
 
+// edge colors
 const EDCOLOR = {
   'white': '#ccc',
   'grey': '#888',
@@ -192,6 +224,8 @@ const DIR = (h: Arrow, t: Arrow): string | undefined =>
 /* --- Dot Model                                                          --- */
 /* -------------------------------------------------------------------------- */
 
+type cluster = { props: Cluster; nodes: Node[]; }
+
 class Builder {
 
   private selected: string | undefined;
@@ -200,6 +234,7 @@ class Builder {
   private kid = 0;
   private imap = new Map<string, string>();
   private rmap = new Map<string, string>();
+  private cmap = new Map<string, cluster>();
 
   index(id: string): string {
     const n = this.imap.get(id);
@@ -210,6 +245,25 @@ class Builder {
     return m;
   }
 
+  findCluster(id: string): cluster {
+    const c = this.cmap.get(id);
+    if (c !== undefined) return c;
+    const d = { props: { id }, nodes: [] };
+    this.cmap.set(id, d);
+    return d;
+  }
+
+  addClusterNode(n: Node): void {
+    if (n.cluster !== undefined) {
+      this.findCluster(n.cluster).nodes.push(n);
+    }
+  }
+
+  setClusterProps(props: Cluster): void {
+    const c = this.findCluster(props.id);
+    c.props = props;
+  }
+
   nodeId(n: string): string {
     return this.rmap.get(n) ?? n;
   }
@@ -217,6 +271,7 @@ class Builder {
   init(): Builder {
     this.spec = 'digraph {\n';
     this.selected = undefined;
+    this.cmap.clear();
     // Keep node index to fade in & out
     return this;
   }
@@ -244,15 +299,15 @@ class Builder {
     return this.print(a.split('"').join('\\"'));
   }
 
-  value(a: string | number): Builder {
+  value(a: string | number | boolean): Builder {
     if (typeof a === 'string')
       return this.print('"').escaped(a).print('"');
     else
       return this.print(`${a}`);
   }
 
-  attr(a: string, v: string | number | undefined): Builder {
-    return v ? this.print(' ', a, '=').value(v).print('; ') : this;
+  attr(a: string, v: string | number | boolean | undefined): Builder {
+    return v ? this.print(' ', a, '=').value(v).print(';') : this;
   }
 
   // --- Node Table Shape
@@ -281,28 +336,54 @@ class Builder {
 
   // --- Node
   node(n: Node): void {
-    this.print('  ').port(n.id).print(' [');
+    this
+      .print('  ')
+      .port(n.id)
+      .print(' [')
+      .attr('id', n.id);
     if (typeof n.shape === 'object') {
       this
         .attr('shape', 'record')
         .print(' label="')
         .record(n.shape)
-        .print('"; ');
+        .print('";')
+        .attr('tooltip', n.title ?? n.id);
     } else {
       this
         .attr('label', n.label ?? n.id)
-        .attr('shape', n.shape);
+        .attr('shape', n.shape)
+        .attr('tooltip', n.title ?? n.label ?? n.id);
     }
     const color = n.color ?? (n.id === this.selected ? 'selected' : 'white');
     this
-      .attr('id', n.id)
-      .attr('tooltip', n.title)
       .attr('fontcolor', FGCOLOR[color])
       .attr('fillcolor', BGCOLOR[color])
-      .println('];');
+      .println(' ];');
+  }
+
+  cluster(c: cluster): void {
+    const { props: s, nodes } = c;
+    const { color = 'grey' } = s;
+    this
+      .print('  subgraph cluster_', this.index(s.id), ' {\n   ')
+      .attr('style', 'filled')
+      .attr('label', s.label)
+      .attr('tooltip', s.title ?? s.id)
+      .attr('fontcolor', FGCOLOR[color])
+      .attr('fillcolor', SGCOLOR[color])
+      .print('\n   ');
+    nodes.forEach(n => this.print(' ', this.index(n.id), ';'));
+    this.println('\n  }');
+  }
+
+  clusters(cs: readonly Cluster[]): Builder {
+    cs.forEach(c => this.setClusterProps(c));
+    return this;
   }
 
   nodes(ns: readonly Node[]): Builder {
+    ns.forEach(n => this.addClusterNode(n));
+    this.cmap.forEach(c => this.cluster(c));
     ns.forEach(n => this.node(n));
     return this;
   }
@@ -310,6 +391,7 @@ class Builder {
   // --- Edge
   edge(e: Edge): void {
     const { line = 'solid', head = 'arrow', tail = 'none' } = e;
+    const tooltip = e.title ?? e.label ?? `${e.source} -> ${e.target}`;
     this
       .print('  ')
       .port(e.source, e.sourcePort)
@@ -319,7 +401,10 @@ class Builder {
       .attr('label', e.label)
       .attr('headlabel', e.headLabel)
       .attr('taillabel', e.tailLabel)
-      .attr('labeltooltip', e.title)
+      .attr('labeltooltip', e.label ? tooltip : undefined)
+      .attr('headtooltip', e.headLabel ? tooltip : undefined)
+      .attr('tailtooltip', e.tailLabel ? tooltip : undefined)
+      .attr('tooltip', tooltip)
       .attr('dir', DIR(head, tail))
       .attr('color', e.color ? EDCOLOR[e.color] : undefined)
       .attr('style', line === 'solid' ? undefined : line)
@@ -350,19 +435,28 @@ function GraphvizView(props: GraphvizProps): JSX.Element {
   const builder = React.useMemo(() => new Builder, []);
 
   // --- Model Generation
-  const { direction = 'LR', nodes, edges, selected } = props;
+  const {
+    direction = 'LR',
+    clusters = [],
+    nodes = [],
+    edges = [],
+    selected
+  } = props;
+
   const model = React.useMemo(() =>
     builder
       .init()
       .select(selected)
+      .print(' ')
       .attr('rankdir', direction)
       .attr('bgcolor', 'none')
       .attr('width', 0.5)
       .println('node [ style="filled" ];')
+      .clusters(clusters)
       .nodes(nodes)
       .edges(edges)
       .flush()
-    , [builder, direction, nodes, edges, selected]
+    , [builder, direction, clusters, nodes, edges, selected]
   );
 
   // --- Model Update Callback
@@ -442,8 +536,7 @@ export function Diagram(props: DiagramProps): JSX.Element {
             </div>
           )}
         </AutoSizer >
-      )
-      }
+      )}
     </>
   );
 }
diff --git a/ivette/src/sandbox/dotdiagram.tsx b/ivette/src/sandbox/dotdiagram.tsx
index 321036f953cf574a519ada6b2d1ef62671ffc63c..191617f28bc1a6299abcf24c7ad53caf922ebd39 100644
--- a/ivette/src/sandbox/dotdiagram.tsx
+++ b/ivette/src/sandbox/dotdiagram.tsx
@@ -27,7 +27,7 @@
 import React from 'react';
 import { Scroll } from 'dome/layout/boxes';
 import { HSplit } from 'dome/layout/splitters';
-import { Diagram, Node, Edge } from 'dome/graph/diagram';
+import { Diagram, Node, Edge, Cluster } from 'dome/graph/diagram';
 import { registerSandbox } from 'ivette';
 
 // --------------------------------------------------------------------------
@@ -45,17 +45,17 @@ const nodes: Node[] = [
       { label: 'Dashed "e"', port: 'e' },
     ]
   },
-  { id: 'white', color: 'white' },
-  { id: 'grey', color: 'grey' },
-  { id: 'dark', color: 'dark' },
-  { id: 'primary', color: 'primary' },
-  { id: 'selected', color: 'selected' },
-  { id: 'green', color: 'green' },
-  { id: 'orange', color: 'orange' },
-  { id: 'red', color: 'red' },
-  { id: 'yellow', color: 'yellow' },
-  { id: 'blue', color: 'blue' },
-  { id: 'pink', color: 'pink' },
+  { id: 'white', color: 'white', cluster: 'BG' },
+  { id: 'grey', color: 'grey', cluster: 'BG' },
+  { id: 'dark', color: 'dark', cluster: 'BG' },
+  { id: 'primary', color: 'primary', cluster: 'BG' },
+  { id: 'selected', color: 'selected', cluster: 'BG' },
+  { id: 'green', color: 'green', cluster: 'BG' },
+  { id: 'orange', color: 'orange', cluster: 'BG' },
+  { id: 'red', color: 'red', cluster: 'BG' },
+  { id: 'yellow', color: 'yellow', cluster: 'BG' },
+  { id: 'blue', color: 'blue', cluster: 'BG' },
+  { id: 'pink', color: 'pink', cluster: 'BG' },
   { id: 'X' }, { id: 'Y' }
 ];
 
@@ -87,9 +87,15 @@ const edges: Edge[] = [
   },
 ];
 
+function makeCluster(s: string | undefined): Cluster {
+  const color = nodes.find(n => n.id === s)?.color;
+  return { id: 'BG', title: 'Background Cluster', color };
+}
+
 function DiagramSample(): JSX.Element {
   const [model, setModel] = React.useState('');
   const [selected, setSelected] = React.useState<string>();
+  const clusters = React.useMemo(() => [makeCluster(selected)], [selected]);
   return (
     <HSplit settings='sandbox.diagram.split'>
       <Scroll>
@@ -101,6 +107,7 @@ function DiagramSample(): JSX.Element {
       <Diagram
         nodes={nodes}
         edges={edges}
+        clusters={clusters}
         selected={selected}
         onModelChanged={setModel}
         onSelection={setSelected}