diff --git a/hugo/content/docs/recipes/custom-views.md b/hugo/content/docs/recipes/custom-views.md
new file mode 100644
index 0000000..5656d3b
--- /dev/null
+++ b/hugo/content/docs/recipes/custom-views.md
@@ -0,0 +1,421 @@
+---
+title: 'Custom Views'
+---
+{{< toc >}}
+
+Custom views are the foundation of how Sprotty renders your diagram elements. Each model element type is associated with a view that defines how it appears in the SVG DOM. This recipe covers everything you need to know about creating, configuring, and composing custom views.
+
+## Understanding Views
+
+Views are classes that implement the `IView` interface and transform model elements into SVG representations. Each view has a `render()` method that returns a virtual DOM node (`VNode`) describing the SVG elements to be rendered.
+
+### The View-Model Relationship
+
+Every model element has a `type` property that maps to exactly one view in the `ViewRegistry`. When Sprotty renders your diagram, it:
+
+1. Traverses the model tree
+2. Looks up each element's view by its type
+3. Calls the view's `render()` method
+4. Builds the SVG DOM from the returned virtual nodes
+
+```typescript
+// Model element with type
+const nodeModel: SNode = {
+ id: 'node1',
+ type: 'node:custom', // This maps to a view
+ position: { x: 100, y: 50 },
+ size: { width: 120, height: 80 }
+};
+```
+
+## Setting Up JSX for Custom Views
+
+Before you can create custom views using JSX/TSX syntax, you need to set up your TypeScript environment properly. Sprotty uses JSX to declaratively define SVG elements, but this requires specific configuration.
+
+### The JSX Pragma
+
+Every view file that uses JSX must start with a special pragma comment at the very top:
+
+```typescript
+/** @jsx svg */
+import { svg } from 'sprotty/lib/lib/jsx';
+```
+
+**The Pragma Comment**: `/** @jsx svg */` is a JSX pragma that tells the TypeScript compiler which function to use when transforming JSX expressions. Instead of using React's `createElement`, we use Sprotty's `svg` function.
+
+**The svg Import**: The `svg` function from `sprotty/lib/lib/jsx` is what actually transforms your JSX expressions into Snabbdom virtual DOM nodes (`VNode`). This is the core of how Sprotty renders SVG elements efficiently.
+
+### Why These Are Required
+
+When you write JSX like ``, the TypeScript compiler needs to transform it into function calls. The pragma tells TypeScript to transform it to `svg('rect', { width: 100, height: 50 })` instead of the default `React.createElement('rect', { width: 100, height: 50 })`.
+
+Without the pragma and import:
+
+- ❌ Your JSX won't compile
+- ❌ You'll get "Cannot find name 'React'" errors
+- ❌ The virtual DOM nodes won't be created correctly
+
+### TypeScript Configuration
+
+Your `tsconfig.json` must have JSX support enabled. Add or verify these settings:
+
+```json
+{
+ "compilerOptions": {
+ "jsx": "react",
+ "experimentalDecorators": true
+ }
+}
+```
+
+- **`"jsx": "react"`**: Enables JSX transformation (despite the name, this works for non-React uses too)
+- **`"experimentalDecorators": true`**: Required for the `@injectable()` decorator used in view classes
+
+### File Extension Requirement
+
+View files that use JSX **must** have the `.tsx` extension, not `.ts`:
+
+- ✅ `views.tsx` - Correct
+- ❌ `views.ts` - Will not compile
+
+> 💡 **Pro Tip**: Always copy the pragma and import as the first two lines of any new view file. This is easy to forget and will cause confusing compilation errors!
+
+## Creating Your First Custom View
+
+Let's create a custom view step by step:
+
+### 1. Basic View Structure
+
+```typescript
+/** @jsx svg */
+import { svg } from 'sprotty/lib/lib/jsx';
+import { injectable } from 'inversify';
+import { VNode } from 'snabbdom';
+import { IView, RenderingContext, SNodeImpl, IViewArgs } from 'sprotty';
+
+@injectable()
+export class CustomNodeView implements IView {
+ render(node: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined {
+ // Check if the element should be rendered
+ if (!this.isVisible(node, context)) {
+ return undefined;
+ }
+
+ // Return the SVG representation
+ return
+
+ {context.renderChildren(node)}
+ ;
+ }
+
+ // Helper method for visibility checking
+ protected isVisible(element: SNodeImpl, context: RenderingContext): boolean {
+ return !element.hidden && context.targetKind !== 'hidden';
+ }
+}
+```
+
+### 2. Key Components Explained
+
+**Injectable Decorator**: The `@injectable()` decorator is required for dependency injection. Never forget this!
+
+**Visibility Check**: Always check if an element should be rendered. This optimizes performance by skipping hidden or out-of-viewport elements.
+
+**Root Element**: The render method must return exactly one root element. Use `` (group) to wrap multiple SVG elements.
+
+**Position**: Set `x="0" y="0"` - the layout engine handles actual positioning.
+
+**Children Rendering**: Use `context.renderChildren(node)` to render child elements in their own views.
+
+## TSX Syntax and Conventions
+
+Sprotty uses TSX (TypeScript + JSX) for defining SVG elements. This allows you to write SVG declaratively with TypeScript expressions.
+
+> 📖 **Note**: If you haven't already, make sure you've set up the JSX pragma and imports as explained in the [Setting Up JSX for Custom Views](#setting-up-jsx-for-custom-views) section above. Without these, the JSX syntax shown below won't work.
+
+### CSS Classes
+
+Use Sprotty's convenient class syntax for dynamic styling (which gets transformed into Snabbdom's class module format):
+
+```typescript
+return 0} // Complex condition → class="error" (if condition is true)
+ class-large={node.size.width > 200} // Size-based styling → class="large" (if width > 200)
+/>;
+// Results in HTML: (assuming conditions are met)
+```
+
+### Attributes and Properties
+
+```typescript
+return ;
+```
+
+### Handling Complex Content
+
+For complex content, use TypeScript expressions:
+
+```typescript
+render(node: Readonly, context: RenderingContext): VNode {
+ const corners = this.calculateCorners(node);
+ const pathData = this.createPathFromCorners(corners);
+
+ return
+
+ {node.showLabel && {node.label}}
+ {context.renderChildren(node)}
+ ;
+}
+```
+
+## View Registry Configuration
+
+Views must be registered in your dependency injection container to be used:
+
+### Basic Registration
+
+```typescript
+import { ContainerModule } from 'inversify';
+import { configureModelElement, TYPES } from 'sprotty';
+
+const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => {
+ const context = { bind, unbind, isBound, rebind };
+
+ // Register model element with its view
+ configureModelElement(context, 'node:custom', SNodeImpl, CustomNodeView);
+ configureModelElement(context, 'edge:custom', SEdgeImpl, CustomEdgeView);
+ configureModelElement(context, 'label:custom', SLabelImpl, CustomLabelView);
+});
+```
+
+### Advanced Registration with Features
+
+```typescript
+configureModelElement(context, 'node:interactive', InteractiveNode, InteractiveNodeView, {
+ enable: [selectFeature, moveFeature, hoverFeedback, popupFeature]
+});
+```
+
+## Rendering Context and Lifecycle
+
+The `RenderingContext` provides essential services and information during rendering:
+
+### Context Properties
+
+```typescript
+render(node: Readonly, context: RenderingContext): VNode {
+ // Check rendering target (main, popup, hidden)
+ if (context.targetKind === 'popup') {
+ return this.renderPopupVersion(node);
+ }
+
+ // Access the model root
+ const root = context.root;
+
+ // Check if we're in hidden rendering (for bounds computation)
+ if (context.targetKind === 'hidden') {
+ return this.renderForBoundsComputation(node);
+ }
+
+ return this.renderNormal(node, context);
+}
+```
+
+### Rendering Children
+
+```typescript
+// Render all children
+{context.renderChildren(node)}
+
+// Render specific child
+{context.renderElement(specificChild)}
+
+// Render children with filtering
+{node.children
+ .filter(child => child.type === 'label')
+ .map(child => context.renderElement(child))}
+```
+
+### Context Services
+
+```typescript
+render(node: Readonly, context: RenderingContext): VNode {
+ // Access view registry for manual view lookup
+ const labelView = context.viewRegistry.get('label:standard');
+
+ // Access parent arguments if available
+ const parentArgs = context.parentArgs;
+
+ return /* ... */;
+}
+```
+
+## View Composition Patterns
+
+### Extending Existing Views
+
+Build upon Sprotty's built-in views:
+
+```typescript
+@injectable()
+export class EnhancedNodeView extends RectangularNodeView {
+ override render(node: Readonly, context: RenderingContext): VNode | undefined {
+ const baseNode = super.render(node, context);
+ if (!baseNode) return undefined;
+
+ // Add custom decorations
+ const decorations = this.renderDecorations(node);
+
+ return
+ {baseNode}
+ {decorations}
+ ;
+ }
+
+ protected renderDecorations(node: SNodeImpl): VNode[] {
+ const decorations: VNode[] = [];
+
+ if (node.selected) {
+ decorations.push();
+ }
+
+ return decorations;
+ }
+}
+```
+
+### Compositional Views
+
+Create reusable view components:
+
+```typescript
+@injectable()
+export class ComplexNodeView implements IView {
+ render(node: Readonly, context: RenderingContext): VNode {
+ return
+ {this.renderHeader(node)}
+ {this.renderBody(node, context)}
+ {this.renderFooter(node)}
+ ;
+ }
+
+ protected renderHeader(node: ComplexNode): VNode {
+ return ;
+ }
+
+ protected renderBody(node: ComplexNode, context: RenderingContext): VNode {
+ return
+ {context.renderChildren(node)}
+ ;
+ }
+
+ protected renderFooter(node: ComplexNode): VNode {
+ return ;
+ }
+}
+```
+
+### Conditional View Logic
+
+Handle different states and configurations:
+
+```typescript
+@injectable()
+export class StatefulNodeView implements IView {
+ render(node: Readonly, context: RenderingContext): VNode {
+ switch (node.state) {
+ case 'loading':
+ return this.renderLoadingState(node);
+ case 'error':
+ return this.renderErrorState(node);
+ case 'success':
+ return this.renderSuccessState(node, context);
+ default:
+ return this.renderDefaultState(node, context);
+ }
+ }
+
+ protected renderLoadingState(node: StatefulNode): VNode {
+ return
+
+
+ Loading...
+
+ ;
+ }
+
+ // ... other state renderers
+}
+```
+
+## Best Practices
+
+### Performance Optimization
+
+1. **Early Returns**: Always check visibility first
+2. **Minimal Calculations**: Cache expensive computations
+3. **Conditional Rendering**: Skip unnecessary elements
+
+```typescript
+render(node: Readonly, context: RenderingContext): VNode | undefined {
+ // Early visibility check
+ if (!this.isVisible(node, context)) {
+ return undefined;
+ }
+
+ // Cache expensive calculations
+ if (!this.cachedPath || this.isDirty(node)) {
+ this.cachedPath = this.calculateComplexPath(node);
+ this.markClean(node);
+ }
+
+ return ;
+}
+```
+
+### Separation of Concerns
+
+1. **Keep views focused**: One view per visual concept
+2. **Delegate to children**: Let child views handle their own rendering
+3. **Extract helpers**: Move complex calculations to separate methods
+
+### Error Handling
+
+```typescript
+render(node: Readonly, context: RenderingContext): VNode | undefined {
+ try {
+ return this.renderContent(node, context);
+ } catch (error) {
+ console.error('Error rendering node:', node.id, error);
+ return this.renderErrorFallback(node);
+ }
+}
+
+protected renderErrorFallback(node: SNodeImpl): VNode {
+ return ;
+}
+```
+
+The key to mastering custom views is practice. Start simple, build incrementally, and always keep the separation between model and view clear.
diff --git a/hugo/data/menu/main.yaml b/hugo/data/menu/main.yaml
index 34cb6d9..81116e4 100644
--- a/hugo/data/menu/main.yaml
+++ b/hugo/data/menu/main.yaml
@@ -29,6 +29,8 @@ main:
ref: "docs/concepts/best-practices"
- name: Recipes
sub:
+ - name: Custom Views
+ ref: "docs/recipes/custom-views"
- name: SVG Rendering
ref: "docs/recipes/svg-rendering"
- name: Micro-layout