blender-syntax-panels
Quick Reference
Critical Warnings
NEVER modify scene data inside Panel.draw() — draw callbacks are read-only. Modifying properties triggers infinite redraw loops.
NEVER call bpy.ops.* inside draw() — operators MUST be exposed as UI buttons, not called directly during drawing.
NEVER omit bl_space_type or bl_region_type — registration fails without both.
NEVER use bl_idname values without the correct type tag (_PT_ for panels, _MT_ for menus, _UL_ for UILists).
ALWAYS use layout.panel() for collapsible sections in Blender 4.1+ instead of registering separate sub-panel classes.
ALWAYS match bl_space_type, bl_region_type, and bl_category between parent and child panels.
Panel Naming Convention
{ADDON_PREFIX}_{TYPE_TAG}_{name}
Type tags:
_PT_ = Panel
_MT_ = Menu
_UL_ = UIList
_HT_ = Header
Examples:
MY_PT_main_panel
MY_MT_object_menu
MY_UL_item_list
Decision Tree: Which Layout Container?
Need horizontal arrangement? → layout.row(align=False)
Need vertical arrangement? → layout.column(align=False)
Need bordered section? → layout.box()
Need percentage split? → layout.split(factor=0.5)
Need grid arrangement? → layout.grid_flow(row_major=True, columns=0)
Need collapsible section?
Blender 4.1+? → layout.panel("ID", text="Header")
Blender < 4.1? → Register a sub-panel with bl_parent_id
Need separator line? → layout.separator()
Need header spacing? → layout.separator_spacer()
Decision Tree: Panel Location
Sidebar (N-panel)?
→ bl_space_type = 'VIEW_3D', bl_region_type = 'UI'
Properties Editor?
→ bl_space_type = 'PROPERTIES', bl_region_type = 'WINDOW'
→ bl_context = 'object' | 'scene' | 'render' | 'data' | ...
Tool Shelf?
→ bl_space_type = 'VIEW_3D', bl_region_type = 'TOOLS'
Node Editor sidebar?
→ bl_space_type = 'NODE_EDITOR', bl_region_type = 'UI'
Image Editor sidebar?
→ bl_space_type = 'IMAGE_EDITOR', bl_region_type = 'UI'
Essential Patterns
Pattern 1: Minimal Panel
# Blender 3.x/4.x/5.x: minimum viable panel
import bpy
class MY_PT_example(bpy.types.Panel):
bl_label = "My Panel" # Header text (REQUIRED)
bl_idname = "MY_PT_example" # Unique ID (REQUIRED)
bl_space_type = 'VIEW_3D' # Editor type (REQUIRED)
bl_region_type = 'UI' # Region type (REQUIRED)
bl_category = "My Tab" # Sidebar tab name (REQUIRED for UI region)
def draw(self, context):
layout = self.layout
layout.label(text="Hello World")
def register():
bpy.utils.register_class(MY_PT_example)
def unregister():
bpy.utils.unregister_class(MY_PT_example)
Pattern 2: Panel with poll() and draw_header()
# Blender 3.x/4.x/5.x: conditional visibility + header checkbox
class MY_PT_conditional(bpy.types.Panel):
bl_label = "Object Info"
bl_idname = "MY_PT_conditional"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "My Tab"
bl_options = {'DEFAULT_CLOSED'} # Start collapsed
@classmethod
def poll(cls, context):
"""Panel is visible ONLY when a mesh object is active."""
return context.active_object is not None and context.active_object.type == 'MESH'
def draw_header(self, context):
"""Draw a checkbox in the panel header."""
self.layout.prop(context.scene.my_settings, "enabled", text="")
def draw(self, context):
layout = self.layout
layout.active = context.scene.my_settings.enabled # Grey out if disabled
obj = context.active_object
layout.prop(obj, "name")
layout.prop(obj, "location")
Pattern 3: Sub-Panels (Parent-Child Hierarchy)
# Blender 3.x/4.x/5.x: sub-panel via bl_parent_id
class MY_PT_parent(bpy.types.Panel):
bl_label = "Main Settings"
bl_idname = "MY_PT_parent"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "My Tab"
def draw(self, context):
self.layout.label(text="Parent content")
class MY_PT_child_a(bpy.types.Panel):
bl_label = "Transform"
bl_idname = "MY_PT_child_a"
bl_space_type = 'VIEW_3D' # MUST match parent
bl_region_type = 'UI' # MUST match parent
bl_category = "My Tab" # MUST match parent
bl_parent_id = "MY_PT_parent" # Links to parent
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
self.layout.prop(context.active_object, "location")
Pattern 4: Collapsible Sections with layout.panel() (Blender 4.1+)
# Blender 4.1+ ONLY: collapsible section WITHOUT separate class registration
class MY_PT_modern(bpy.types.Panel):
bl_label = "Modern Panel"
bl_idname = "MY_PT_modern"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "My Tab"
def draw(self, context):
layout = self.layout
layout.label(text="Always visible")
# layout.panel() returns (header_layout, panel_layout) or (header_layout, None)
header, body = layout.panel("MY_PT_modern_section", default_closed=True)
header.label(text="Collapsible Section")
if body is not None:
body.prop(context.active_object, "name")
body.prop(context.active_object, "location")
Pattern 5: UILayout API: Common Elements
# Blender 3.x/4.x/5.x: UILayout methods
def draw(self, context):
layout = self.layout
obj = context.active_object
props = context.scene.my_settings
# Property split (label left, widget right)
layout.use_property_split = True
layout.use_property_decorate = True # Show keyframe dots
# Row (horizontal)
row = layout.row(align=True)
row.prop(obj, "location", index=0, text="X")
row.prop(obj, "location", index=1, text="Y")
row.prop(obj, "location", index=2, text="Z")
# Column (vertical)
col = layout.column(align=True)
col.prop(props, "count")
col.prop(props, "scale")
# Box (bordered container)
box = layout.box()
box.label(text="Section", icon='PREFERENCES')
box.prop(props, "option_a")
# Split (percentage-based columns)
split = layout.split(factor=0.3)
split.column().label(text="Label:")
split.column().prop(props, "value", text="")
# Grid flow
grid = layout.grid_flow(row_major=True, columns=3, even_columns=True)
for i in range(9):
grid.label(text=f"Item {i}")
# Separator
layout.separator()
# Operator button
op = layout.operator("my.operator", text="Run", icon='PLAY')
op.my_prop = 42 # Set operator property BEFORE execution
# Conditional enable/disable
row = layout.row()
row.enabled = props.enabled
row.operator("my.action")
# Alert styling (red highlight)
row = layout.row()
row.alert = True
row.label(text="Warning!", icon='ERROR')
Pattern 6: Menu Definition
# Blender 3.x/4.x/5.x: custom menu
class MY_MT_example(bpy.types.Menu):
bl_label = "My Menu"
bl_idname = "MY_MT_example"
def draw(self, context):
layout = self.layout
layout.operator("my.action_a", text="Action A", icon='FILE')
layout.operator("my.action_b", text="Action B")
layout.separator()
layout.menu("MY_MT_submenu") # Nested submenu
layout.operator("wm.call_menu", text="Open Other").name = "MY_MT_other"
@classmethod
def poll(cls, context):
return context.active_object is not None
# Invoke from panel:
# layout.menu("MY_MT_example")
# Append to existing Blender menu:
def draw_my_menu_item(self, context):
self.layout.menu("MY_MT_example")
bpy.types.VIEW3D_MT_object.append(draw_my_menu_item)
# Remove in unregister():
bpy.types.VIEW3D_MT_object.remove(draw_my_menu_item)
Pattern 7: UIList
# Blender 3.x/4.x/5.x: custom list widget
class MY_UL_items(bpy.types.UIList):
bl_idname = "MY_UL_items"
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row(align=True)
row.prop(item, "name", text="", emboss=False)
row.prop(item, "enabled", text="")
elif self.layout_type == 'GRID':
layout.alignment = 'CENTER'
layout.label(text=item.name, icon_value=icon)
def filter_items(self, context, data, propname):
items = getattr(data, propname)
flt_flags = [self.bitflag_filter_item] * len(items)
flt_neworder = list(range(len(items)))
# Filter by name
if self.filter_name:
for i, item in enumerate(items):
if self.filter_name.lower() not in item.name.lower():
flt_flags[i] = 0
return flt_flags, flt_neworder
# Use in panel draw():
# layout.template_list("MY_UL_items", "", data, "items", data, "active_index")
Pattern 8: Properties Editor Panel
# Blender 3.x/4.x/5.x: panel in Properties editor
class MY_PT_object_props(bpy.types.Panel):
bl_label = "Custom Properties"
bl_idname = "MY_PT_object_props"
bl_space_type = 'PROPERTIES' # Properties editor
bl_region_type = 'WINDOW' # Main area (NOT 'UI')
bl_context = "object" # Object properties tab
@classmethod
def poll(cls, context):
return context.active_object is not None
def draw(self, context):
self.layout.prop(context.active_object, "name")
bl_space_type Values
| Value | Editor |
|---|---|
'VIEW_3D' |
3D Viewport |
'PROPERTIES' |
Properties editor |
'OUTLINER' |
Outliner |
'NODE_EDITOR' |
Node editor (shader, compositor, geometry) |
'TEXT_EDITOR' |
Text editor |
'IMAGE_EDITOR' |
UV/Image editor |
'SEQUENCE_EDITOR' |
Video Sequencer |
'CLIP_EDITOR' |
Movie Clip editor |
'PREFERENCES' |
Preferences window |
'GRAPH_EDITOR' |
Graph editor (FCurves) |
'DOPESHEET_EDITOR' |
Dope Sheet |
'NLA_EDITOR' |
NLA editor |
'FILE_BROWSER' |
File browser |
'SPREADSHEET' |
Spreadsheet (Blender 3.0+) |
bl_region_type Values
| Value | Location |
|---|---|
'UI' |
Sidebar (N-panel) |
'TOOLS' |
Tool shelf (T-panel) |
'HEADER' |
Header bar |
'WINDOW' |
Main area (Properties editor panels) |
'TOOL_PROPS' |
Active tool properties |
'TOOL_HEADER' |
Tool header (Blender 2.83+) |
'EXECUTE' |
Operator redo region |
'FOOTER' |
Footer bar |
'NAVIGATION_BAR' |
Navigation bar (Preferences) |
bl_context Values (Properties editor only)
| Value | Tab |
|---|---|
"render" |
Render properties |
"output" |
Output properties |
"view_layer" |
View Layer properties |
"scene" |
Scene properties |
"world" |
World properties |
"object" |
Object properties |
"modifier" |
Modifier properties |
"particle" |
Particle properties |
"physics" |
Physics properties |
"constraint" |
Constraint properties |
"data" |
Object data properties (mesh, curve, etc.) |
"material" |
Material properties |
"texture" |
Texture properties |
"bone" |
Bone properties |
"bone_constraint" |
Bone constraint properties |
bl_options Values (Panel)
| Value | Effect |
|---|---|
'DEFAULT_CLOSED' |
Panel starts collapsed |
'HIDE_HEADER' |
No header bar (cannot collapse) |
'INSTANCED' |
Panel can be used as template in multiple contexts |
'HEADER_LAYOUT_EXPAND' |
Header expands to fill available space |
Common Operations
Adding Popover Panels
# Blender 3.x/4.x/5.x: popover panel (floating panel from header button)
class MY_PT_popover(bpy.types.Panel):
bl_label = "Popover Settings"
bl_idname = "MY_PT_popover"
bl_space_type = 'VIEW_3D'
bl_region_type = 'WINDOW' # WINDOW for popover panels
bl_options = {'INSTANCED'} # Required for popover usage
def draw(self, context):
self.layout.prop(context.scene, "frame_current")
# In a header or panel draw():
# layout.popover("MY_PT_popover", text="Settings")
Pie Menus
# Blender 3.x/4.x/5.x: pie menu
class MY_MT_pie(bpy.types.Menu):
bl_label = "My Pie Menu"
bl_idname = "MY_MT_pie"
def draw(self, context):
pie = self.layout.menu_pie()
pie.operator("transform.translate") # West (left)
pie.operator("transform.rotate") # East (right)
pie.operator("transform.resize") # South (bottom)
pie.operator("object.shade_smooth") # North (top)
# Additional items: NW, NE, SW, SE
# Invoke: bpy.ops.wm.call_menu_pie(name="MY_MT_pie")
Dynamic Panel Content
# Blender 3.x/4.x/5.x: panel content based on selection
def draw(self, context):
layout = self.layout
obj = context.active_object
if obj is None:
layout.label(text="No object selected", icon='INFO')
return
layout.prop(obj, "name")
if obj.type == 'MESH':
layout.label(text=f"Vertices: {len(obj.data.vertices)}")
layout.label(text=f"Faces: {len(obj.data.polygons)}")
elif obj.type == 'LIGHT':
layout.prop(obj.data, "energy")
layout.prop(obj.data, "color")
elif obj.type == 'CAMERA':
layout.prop(obj.data, "lens")
Version-Specific Notes
| Feature | Version | Notes |
|---|---|---|
layout.panel() |
4.1+ | Inline collapsible sections without class registration |
bl_order |
3.0+ | Integer controlling panel sort order |
SPREADSHEET space type |
3.0+ | New editor type |
TOOL_HEADER region |
2.83+ | Separate tool header region |
| Sub-panel support | 2.80+ | bl_parent_id for panel hierarchy |
Reference Links
- references/methods.md — Complete API signatures for Panel, UILayout, Menu, UIList
- references/examples.md — Working code examples for common panel patterns
- references/anti-patterns.md — What NOT to do when creating panels and UI