How to create new node socket panels?

Blender 4.0 introduced new collapsible panels within nodes, but I can’t find any documentation about how to actually create them for custom nodes though. Does anyone know how to do this? Thanks so much!

Do I understand correctly that you are asking about creating a node interface panel?

It looks like it, yes! Part of my confusion might be because there’s no clear name for what they’re actually supposed to be called so my Google-fu is failing.

Hi, I’m running with the same issue. Would you be able to share what you found please?

I ended up reaching out to the dev who wrote the feature, and he said that as of right now panels in custom nodes are not supported for architectural reasons. So at the moment we’re just out of luck, unfortunately. You can use them in node groups, though, both in Python and in the user interface.

You have any idea if this is yet available?

After some digging in blender source folder. the node.py file does have an operator which seems able to move items to a panel. Ive been trying to use it, but without luck. Getting the correct context here is the big issue

The Class im talking about is called “NODE_OT_interface_item_new”

class NODE_OT_interface_item_new(NodeInterfaceOperator, Operator):
    '''Add a new item to the interface'''
    bl_idname = "node.interface_item_new"
    bl_label = "New Item"
    bl_options = {'REGISTER', 'UNDO'}

    item_type: EnumProperty(
        name="Item Type",
        description="Type of the item to create",
        items=(
            ('INPUT', "Input", ""),
            ('OUTPUT', "Output", ""),
            ('PANEL', "Panel", ""),
        ),
        default='INPUT',
    )

    # Returns a valid socket type for the given tree or None.
    @staticmethod
    def find_valid_socket_type(tree):
        socket_type = 'NodeSocketFloat'
        # Socket type validation function is only available for custom
        # node trees. Assume that 'NodeSocketFloat' is valid for
        # built-in node tree types.
        if not hasattr(tree, "valid_socket_type") or tree.valid_socket_type(socket_type):
            return socket_type
        # Custom nodes may not support float sockets, search all
        # registered socket subclasses.
        types_to_check = [bpy.types.NodeSocket]
        while types_to_check:
            t = types_to_check.pop()
            idname = getattr(t, "bl_idname", "")
            if tree.valid_socket_type(idname):
                return idname
            # Test all subclasses
            types_to_check.extend(t.__subclasses__())

    def execute(self, context):
        snode = context.space_data
        tree = snode.edit_tree
        interface = tree.interface

        # Remember active item and position to determine target position.
        active_item = interface.active
        active_pos = active_item.position if active_item else -1

        if self.item_type == 'INPUT':
            item = interface.new_socket("Socket", socket_type=self.find_valid_socket_type(tree), in_out='INPUT')
        elif self.item_type == 'OUTPUT':
            item = interface.new_socket("Socket", socket_type=self.find_valid_socket_type(tree), in_out='OUTPUT')
        elif self.item_type == 'PANEL':
            item = interface.new_panel("Panel")
        else:
            return {'CANCELLED'}

        if active_item:
            # Insert into active panel if possible, otherwise insert after active item.
            if active_item.item_type == 'PANEL' and item.item_type != 'PANEL':
                interface.move_to_parent(item, active_item, len(active_item.interface_items))
            else:
                interface.move_to_parent(item, active_item.parent, active_pos + 1)
        interface.active = item

        return {'FINISHED'}

I haven’t thought about this in a long time. I haven’t seen anything to indicate it is possible though, unfortunately. I’d love to see this happen for custom Python nodes.

Great! Ill try this

I also found an Addon called NodesToPython, which showed me another approach

import bpy, mathutils

# Generate unique scene name
base_name = "LG_View Layer"
end_name = base_name
if bpy.data.scenes.get(end_name) != None:
    i = 1
    end_name = base_name + f".{i:03d}"
    while bpy.data.scenes.get(end_name) != None:
        end_name = base_name + f".{i:03d}"
        i += 1

scene = bpy.context.window.scene.copy()

scene.name = end_name
scene.use_fake_user = True
bpy.context.window.scene = scene
#initialize LG_View Layer node group
def lg_view_layer_node_group():
    lg_view_layer = scene.node_tree
    #start with a clean node tree
    for node in lg_view_layer.nodes:
        lg_view_layer.nodes.remove(node)
    lg_view_layer.color_tag = 'NONE'
    lg_view_layer.description = ""
    lg_view_layer.default_group_node_width = 140
    

    # # Create NodeGroup
    # nt = context.scene.node_tree
    # viewlayer_node = nt.nodes.active
    # viewlayer_name = viewlayer_node.layer


    # create group node
    group_node = lg_view_layer.nodes.new(type='CompositorNodeGroup')
    group_node.location = [0,0] #(viewlayer_node.location[0] + 300, viewlayer_node.location[1])
    group_nt = bpy.data.node_groups.new(name='LG_' + base_name, type='CompositorNodeTree')
    group_node.node_tree = group_nt

    lg_view_layer = group_nt
    #lg_view_layer interface
    #Socket Outout
    outout_socket = lg_view_layer.interface.new_socket(name = "Outout", in_out='OUTPUT', socket_type = 'NodeSocketColor')
    outout_socket.default_value = (0.0, 0.0, 0.0, 1.0)
    outout_socket.attribute_domain = 'POINT'

    #Panel Area
    area_panel = lg_view_layer.interface.new_panel("Area")
    area_panel.description = "Point"
    #Socket Factor
    factor_socket = lg_view_layer.interface.new_socket(name = "Factor", in_out='INPUT', socket_type = 'NodeSocketFloat', parent = area_panel)
    factor_socket.default_value = 0.0
    factor_socket.min_value = 0.0
    factor_socket.max_value = 1.0
    factor_socket.subtype = 'FACTOR'
    factor_socket.attribute_domain = 'POINT'

    #Socket Color
    color_socket = lg_view_layer.interface.new_socket(name = "Color", in_out='INPUT', socket_type = 'NodeSocketColor', parent = area_panel)
    color_socket.default_value = (0.0, 0.0, 0.0, 0.0)
    color_socket.attribute_domain = 'POINT'

    #Socket Area Lights
    area_lights_socket = lg_view_layer.interface.new_socket(name = "Area Lights", in_out='INPUT', socket_type = 'NodeSocketColor', parent = area_panel)
    area_lights_socket.default_value = (0.0, 0.0, 0.0, 1.0)
    area_lights_socket.attribute_domain = 'POINT'

Also in this post, i got different method;

# Create a new panel
panel = group.interface.new_panel(name="My panel")

# Move socket to the panel
group.interface.move_to_parent(
    item=fac_input,
    parent=panel,
    to_position=0
)

# Or assign a parent when creating a new socket
col_input = group.interface.new_socket(
    name="Color",
    in_out='INPUT',
    socket_type='NodeSocketColor',
    parent=panel
)