---
title: Custom Node
order: 12
---
G6 provides a series of [built-in nodes](/en/manual/element/node/base-node), including [circle (Circle Node)](/en/manual/element/node/circle), [diamond (Diamond Node)](/en/manual/element/node/diamond), [donut (Donut Node)](/en/manual/element/node/donut), [ellipse (Ellipse Node)](/en/manual/element/node/ellipse), [hexagon (Hexagon Node)](/en/manual/element/node/hexagon), [html (HTML Node)](/en/manual/element/node/html), [image (Image Node)](/en/manual/element/node/image), [rect (Rectangle Node)](/en/manual/element/node/rect), [star (Star Node)](/en/manual/element/node/star), and [triangle (Triangle Node)](/en/manual/element/node/triangle). These built-in nodes can meet most basic scenario requirements.
However, in actual projects, you may encounter needs that these basic nodes cannot satisfy. In such cases, you need to create custom nodes. Don't worry, this is simpler than you might think!
## Ways to Create Custom Nodes Choose the Right Approach
There are mainly two approaches to creating custom nodes:
### 1. Inherit from Existing Node Types Recommended
This is the most commonly used approach, where you can choose to inherit from one of the following types:
- [`BaseNode`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/base-node.ts) - The most basic node class, providing core node functionality
- [`Circle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/circle.ts) - Circle node
- [`Rect`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/rect.ts) - Rectangle node
- [`Ellipse`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/ellipse.ts) - Ellipse node
- [`Diamond`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/diamond.ts) - Diamond node
- [`Triangle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/triangle.ts) - Triangle node
- [`Star`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/star.ts) - Star node
- [`Image`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/image.ts) - Image node
- [`Donut`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/donut.ts) - Donut node
- [`Hexagon`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/hexagon.ts) - Hexagon node
**Why choose this approach?**
- 📌 **Less Code**: Reuse properties and methods of existing nodes, only focus on new features
- 📌 **Rapid Development**: Suitable for most project needs, quickly achieve business goals
- 📌 **Easy Maintenance**: Clear code structure with well-defined inheritance relationships
:::tip{title=Get Started Now}
If you choose to inherit from existing node types (recommended), you can jump directly to [Create Your First Custom Node in Three Steps](#create-your-first-custom-node-in-three-steps) to start practicing. Most users will choose this approach!
:::
### 2. Build from Scratch Based on G Graphics System Advanced Usage
If existing node types don't meet your requirements, you can create nodes from scratch based on G's underlying graphics system.
**Why choose this approach?**
- 📌 **Maximum Freedom**: Complete control over every detail of the node, achieving any complex effects
- 📌 **Special Requirements**: Highly customized scenarios that existing node types cannot satisfy
- 📌 **Performance Optimization**: Performance optimization for specific scenarios
:::warning{title=Important Notes}
Custom nodes built from scratch require handling all details yourself, including graphics rendering, event response, state changes, etc., with higher development difficulty. You can refer directly to the [source code](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/base-node.ts) for implementation.
:::
## Create Your First Custom Node in Three Steps
Let's start with a simple example - creating a **rectangle node with main and subtitle**:
```js | ob { pin:false, inject: true }
import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
// Step 1: Create custom node class
class DualLabelNode extends Rect {
// Subtitle style
getSubtitleStyle(attributes) {
return {
x: 0,
y: 45, // Place below the main title
text: attributes.subtitle || '',
fontSize: 12,
fill: '#666',
textAlign: 'center',
textBaseline: 'middle',
};
}
// Draw subtitle
drawSubtitleShape(attributes, container) {
const subtitleStyle = this.getSubtitleStyle(attributes);
this.upsert('subtitle', 'text', subtitleStyle, container);
}
// Render method
render(attributes = this.parsedAttributes, container) {
// 1. Render basic rectangle and main title
super.render(attributes, container);
// 2. Add subtitle
this.drawSubtitleShape(attributes, container);
}
}
// Step 2: Register custom node
register(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);
// Step 3: Use custom node
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'node1',
style: { x: 100, y: 100 },
data: {
title: 'Node A', // Main title
subtitle: 'Your First Custom Node', // Subtitle
},
},
],
},
node: {
type: 'dual-label-node',
style: {
fill: '#7FFFD4',
stroke: '#5CACEE',
lineWidth: 2,
radius: 5,
// Main title style
labelText: (d) => d.data.title,
labelFill: '#222',
labelFontSize: 14,
labelFontWeight: 500,
// Subtitle
subtitle: (d) => d.data.subtitle,
},
},
});
graph.render();
```
### Step 1: Write Custom Node Class
Inherit from G6's `Rect` (rectangle node) and add a subtitle:
```js
import { Rect, register, Graph, ExtensionCategory } from '@antv/g6';
// Create custom node, inheriting from Rect
class DualLabelNode extends Rect {
// Subtitle style
getSubtitleStyle(attributes) {
return {
x: 0,
y: 45, // Place below the main title
text: attributes.subtitle || '',
fontSize: 12,
fill: '#666',
textAlign: 'center',
textBaseline: 'middle',
};
}
// Draw subtitle
drawSubtitleShape(attributes, container) {
const subtitleStyle = this.getSubtitleStyle(attributes);
this.upsert('subtitle', 'text', subtitleStyle, container);
}
// Render method
render(attributes = this.parsedAttributes, container) {
// 1. Render basic rectangle and main title
super.render(attributes, container);
// 2. Add subtitle
this.drawSubtitleShape(attributes, container);
}
}
```
### Step 2: Register Custom Node
Use the `register` method to register the node type so that G6 can recognize your custom node:
```js
register(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);
```
The `register` method requires three parameters:
- Extension category: `ExtensionCategory.NODE` indicates this is a node type
- Type name: `dual-label-node` is the name we give to this custom node, which will be used in configuration later
- Class definition: `DualLabelNode` is the node class we just created
### Step 3: Apply Custom Node
Use the custom node in graph configuration:
```js
const graph = new Graph({
data: {
nodes: [
{
id: 'node1',
style: { x: 100, y: 100 },
data: {
title: 'Node A', // Main title
subtitle: 'Your First Custom Node', // Subtitle
},
},
],
},
node: {
type: 'dual-label-node',
style: {
fill: '#7FFFD4',
stroke: '#5CACEE',
lineWidth: 2,
radius: 8,
// Main title style
labelText: (d) => d.data.title,
labelFill: '#222',
labelFontSize: 14,
labelFontWeight: 500,
// Subtitle
subtitle: (d) => d.data.subtitle,
},
},
});
graph.render();
```
🎉 Congratulations! You have created your first custom node. It looks simple, but this process contains the core concept of custom nodes: **inherit from a basic node type**, then **override the `render` method** to add custom content.
## Understanding Data Flow: How to Access Data in Custom Nodes
Before creating complex custom nodes, understanding how data flows into custom nodes is very important. G6 provides multiple ways to access data for custom nodes:
### Method 1: Through `attributes` Parameter (Recommended)
The first parameter `attributes` of the `render` method contains processed style attributes, including data-driven styles:
```js
class CustomNode extends Rect {
render(attributes, container) {
// attributes contains all style attributes, including data-driven styles
console.log('All properties of current node:', attributes);
// If customData: (d) => d.data.someValue is defined in style
// Then you can access it through attributes.customData
const customValue = attributes.customData;
super.render(attributes, container);
}
}
```
### Method 2: Through `this.context.graph` to Access Raw Data
When you need to access the node's raw data, you can get it through the graph instance:
```js
class CustomNode extends Rect {
// Convenient data access method
get nodeData() {
return this.context.graph.getNodeData(this.id);
}
get data() {
return this.nodeData.data || {};
}
render(attributes, container) {
// Get complete node data
const nodeData = this.nodeData;
console.log('Complete node data:', nodeData);
// Get business data from data field
const businessData = this.data;
console.log('Business data:', businessData);
super.render(attributes, container);
}
}
```
### Complete Data Flow Process
Let's understand how data flows from graph data to custom nodes through a specific example:
```js | ob { inject: true }
import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
class DataFlowNode extends Rect {
// Method 2: Get raw data through graph
get nodeData() {
return this.context.graph.getNodeData(this.id);
}
get data() {
return this.nodeData.data || {};
}
render(attributes, container) {
// Method 1: Get processed styles from attributes
console.log('Get from attributes:', {
iconUrl: attributes.iconUrl,
userName: attributes.userName,
});
// Method 2: Get from raw data
console.log('Get from raw data:', {
icon: this.data.icon,
name: this.data.name,
role: this.data.role,
});
// Render basic rectangle
super.render(attributes, container);
// Use data to render custom content
if (attributes.iconUrl) {
this.upsert(
'icon',
'image',
{
x: -25,
y: -12,
width: 20,
height: 20,
src: attributes.iconUrl,
},
container,
);
}
if (attributes.userName) {
this.upsert(
'username',
'text',
{
x: 10,
y: 0,
text: attributes.userName,
fontSize: 10,
fill: '#666',
textAlign: 'center',
textBaseline: 'middle',
},
container,
);
}
}
}
register(ExtensionCategory.NODE, 'data-flow-node', DataFlowNode);
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'user1',
style: { x: 100, y: 100 },
// This is the node's business data
data: {
name: 'Zhang San',
role: 'Developer',
icon: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
},
},
],
},
node: {
type: 'data-flow-node',
style: {
size: [80, 40],
fill: '#f0f9ff',
stroke: '#0ea5e9',
lineWidth: 1,
radius: 4,
// Map data from data field to style attributes
iconUrl: (d) => d.data.icon, // This becomes attributes.iconUrl
userName: (d) => d.data.name, // This becomes attributes.userName
// Main title uses role information
labelText: (d) => d.data.role,
labelFontSize: 12,
labelFill: '#0369a1',
},
},
});
graph.render();
```
:::tip{title=Data Flow Summary}
1. **Graph Data Definition**: Define business data in `data.nodes[].data`
2. **Style Mapping**: Use functions in `node.style` to map data to style attributes
3. **Node Access**: Access data in custom nodes through `attributes` or `this.context.graph`
4. **Rendering Usage**: Use the obtained data to render custom graphics
:::
## From Simple to Complex: Gradually Building Feature-Rich Nodes
Let's gradually increase the complexity and functionality of nodes through practical examples.
### Example 1: User Card Node with Icon and Badge
This example shows how to create a user card node containing avatar, name, and status badge:
```js | ob { inject: true }
import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
class UserCardNode extends Rect {
get nodeData() {
return this.context.graph.getNodeData(this.id);
}
get data() {
return this.nodeData.data || {};
}
// Avatar style
getAvatarStyle(attributes) {
const [width, height] = this.getSize(attributes);
return {
x: -width / 2 + 20,
y: -height / 2 + 15,
width: 30,
height: 30,
src: attributes.avatarUrl || '',
radius: 15, // Circular avatar
};
}
drawAvatarShape(attributes, container) {
if (!attributes.avatarUrl) return;
const avatarStyle = this.getAvatarStyle(attributes);
this.upsert('avatar', 'image', avatarStyle, container);
}
// Status badge style
getBadgeStyle(attributes) {
const [width, height] = this.getSize(attributes);
const status = this.data.status || 'offline';
const colorMap = {
online: '#52c41a',
busy: '#faad14',
offline: '#8c8c8c',
};
return {
x: width / 2 - 8,
y: -height / 2 + 8,
r: 4,
fill: colorMap[status],
stroke: '#fff',
lineWidth: 2,
};
}
drawBadgeShape(attributes, container) {
const badgeStyle = this.getBadgeStyle(attributes);
this.upsert('badge', 'circle', badgeStyle, container);
}
// Username style
getUsernameStyle(attributes) {
const [width, height] = this.getSize(attributes);
return {
x: -width / 2 + 55,
y: -height / 2 + 20,
text: attributes.username || '',
fontSize: 14,
fill: '#262626',
fontWeight: 'bold',
textAlign: 'left',
textBaseline: 'middle',
};
}
drawUsernameShape(attributes, container) {
if (!attributes.username) return;
const usernameStyle = this.getUsernameStyle(attributes);
this.upsert('username', 'text', usernameStyle, container);
}
// Role label style
getRoleStyle(attributes) {
const [width, height] = this.getSize(attributes);
return {
x: -width / 2 + 55,
y: -height / 2 + 35,
text: attributes.userRole || '',
fontSize: 11,
fill: '#8c8c8c',
textAlign: 'left',
textBaseline: 'middle',
};
}
drawRoleShape(attributes, container) {
if (!attributes.userRole) return;
const roleStyle = this.getRoleStyle(attributes);
this.upsert('role', 'text', roleStyle, container);
}
render(attributes, container) {
// Render basic rectangle
super.render(attributes, container);
// Add various components
this.drawAvatarShape(attributes, container);
this.drawBadgeShape(attributes, container);
this.drawUsernameShape(attributes, container);
this.drawRoleShape(attributes, container);
}
}
register(ExtensionCategory.NODE, 'user-card-node', UserCardNode);
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'user1',
style: { x: 100, y: 100 },
data: {
name: 'Zhang Xiaoming',
role: 'Frontend Engineer',
status: 'online',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang',
},
},
],
},
node: {
type: 'user-card-node',
style: {
size: [140, 50],
fill: '#ffffff',
stroke: '#d9d9d9',
lineWidth: 1,
radius: 6,
// Data mapping
avatarUrl: (d) => d.data.avatar,
username: (d) => d.data.name,
userRole: (d) => d.data.role,
},
},
});
graph.render();
```
### Example 2: Node with Clickable Action Button
Add a blue button to the node that triggers events (prints logs or executes callbacks) when clicked.
```js | ob { inject: true }
import { Graph, register, Rect, ExtensionCategory } from '@antv/g6';
class ClickableNode extends Rect {
getButtonStyle(attributes) {
return {
x: 40,
y: -10,
width: 20,
height: 20,
radius: 10,
fill: '#1890ff',
cursor: 'pointer', // Mouse pointer becomes hand
};
}
drawButtonShape(attributes, container) {
const btnStyle = this.getButtonStyle(attributes, container);
const btn = this.upsert('button', 'rect', btnStyle, container);
// Add click event to button
if (!btn.__clickBound) {
btn.addEventListener('click', (e) => {
// Prevent event bubbling to avoid triggering node click event
e.stopPropagation();
// Execute business logic
console.log('Button clicked on node:', this.id);
// If there's a callback function in data, call it
if (typeof attributes.onButtonClick === 'function') {
attributes.onButtonClick(this.id, this.data);
}
});
btn.__clickBound = true; // Mark as bound to avoid duplicate binding
}
}
render(attributes, container) {
super.render(attributes, container);
// Add a button
this.drawButtonShape(attributes, container);
}
}
register(ExtensionCategory.NODE, 'clickable-node', ClickableNode);
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [
{
id: 'node1',
style: { x: 100, y: 100 },
},
],
},
node: {
type: 'clickable-node', // Specify using our custom node
style: {
size: [60, 30],
fill: '#7FFFD4',
stroke: '#5CACEE',
lineWidth: 2,
radius: 5,
onButtonClick: (id, data) => {},
},
},
});
graph.render();
```
### Example 3: Node Responding to State Changes (Click to Change Color)
Common interactions require nodes and edges to provide feedback through style changes, such as when the mouse moves over a node, clicking to select nodes/edges, or activating interactions on edges through interaction. All these require changing the styles of nodes and edges. There are two ways to achieve this effect:
1. Get the current state from `data.states` and handle state changes in the custom node class;
2. Separate interaction state from raw data and node drawing logic, only update the node.
We recommend users use the second approach to implement node state adjustments, which can be achieved through the following steps:
1. Implement custom node;
2. Configure node state styles in graph configuration;
3. Set node state through the `graph.setElementState()` method.
Based on rect, extend a hole shape with default white fill color that turns orange when clicked. The sample code to achieve this effect is as follows:
```js | ob { inject: true }
import { Rect, register, Graph, ExtensionCategory } from '@antv/g6';
// 1. Define node class
class SelectableNode extends Rect {
getHoleStyle(attributes) {
return {
x: 20,
y: -10,
radius: 10,
width: 20,
height: 20,
fill: attributes.holeFill,
};
}
drawHoleShape(attributes, container) {
const holeStyle = this.getHoleStyle(attributes, container);
this.upsert('hole', 'rect', holeStyle, container);
}
render(attributes, container) {
super.render(attributes, container);
this.drawHoleShape(attributes, container);
}
}
// 2. Register node
register(ExtensionCategory.NODE, 'selectable-node', SelectableNode, true);
// 3. Create graph instance
const graph = new Graph({
container: 'container',
height: 200,
data: {
nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],
},
node: {
type: 'selectable-node',
style: {
size: [120, 60],
radius: 6,
fill: '#7FFFD4',
stroke: '#5CACEE',
lineWidth: 2,
holeFill: '#fff',
},
state: {
// Mouse selected state
selected: {
holeFill: 'orange',
},
},
},
});
// 4. Add node interaction
graph.on('node:click', (evt) => {
const nodeId = evt.target.id;
graph.setElementState(nodeId, ['selected']);
});
graph.render();
```