Skip to main content

Overview

NVDA Objects represent controls and widgets in applications. By creating custom NVDA Object classes (overlay classes), you can:
  • Enhance accessibility of poorly accessible controls
  • Provide better names, descriptions, and role information
  • Customize how text content is accessed
  • Add custom scripts and event handlers to specific controls
  • Fix broken accessibility implementations
Custom NVDA Objects use the overlay class pattern - you create classes that add functionality without modifying the base implementation.

NVDA Object Hierarchy

Every widget is represented by an NVDA Object that provides:
name
str
The label/name of the control (e.g., “OK”, “File name”)
role
Role
The type of control (from controlTypes.Role: BUTTON, EDITABLETEXT, CHECKBOX, etc.)
value
str
The current value (e.g., percentage of scrollbar, content of edit field)
states
set[State]
Current states (from controlTypes.State: FOCUSED, SELECTED, CHECKED, etc.)
description
str
Additional descriptive information (usually from tooltip)
location
tuple
Screen coordinates: (left, top, width, height)
navigation.py
# Hierarchy navigation
obj.parent          # Parent object
obj.firstChild      # First child object
obj.lastChild       # Last child object
obj.children        # List of all children
obj.next            # Next sibling
obj.previous        # Previous sibling

# Simplified navigation (filters out unimportant objects)
obj.simpleParent
obj.simpleFirstChild
obj.simpleLastChild
obj.simpleNext
obj.simplePrevious

Creating Overlay Classes

Overlay classes are chosen through the chooseNVDAObjectOverlayClasses method in app modules or global plugins:
import appModuleHandler
from NVDAObjects.IAccessible import IAccessible
import controlTypes

class AppModule(appModuleHandler.AppModule):

	def chooseNVDAObjectOverlayClasses(self, obj, clsList):
		"""Choose overlay classes for objects in this application."""
		if obj.windowClassName == "MyCustomButton":
			clsList.insert(0, CustomButton)
		elif obj.role == controlTypes.Role.LIST:
			clsList.insert(0, EnhancedList)

class CustomButton(IAccessible):
	"""Custom button with enhanced functionality."""
	
	def _get_name(self):
		"""Provide better name."""
		# Custom logic to extract name
		name = super().name
		return f"Button: {name}"

class EnhancedList(IAccessible):
	"""Enhanced list with additional information."""
	
	def event_gainFocus(self):
		"""Custom focus announcement."""
		super().event_gainFocus()
		# Additional information
		import ui
		ui.message(f"List with {self.childCount} items")

Overlay Class Rules

  • Insert overlay classes at the beginning of clsList (index 0)
  • Overlay classes must inherit from an appropriate base class (usually the API class like IAccessible or Window)
  • Multiple overlay classes can be added; they’re resolved in order
  • More specific classes should be inserted earlier

Customizing Properties

Override property getters to customize object information:
from NVDAObjects.IAccessible import IAccessible
import controlTypes

class CustomControl(IAccessible):
	"""Custom control with modified properties."""
	
	def _get_name(self):
		"""Override name property."""
		# Get name from custom location
		name = self._getCustomName()
		return name or super().name
	
	def _get_role(self):
		"""Override role."""
		return controlTypes.Role.BUTTON
	
	def _get_description(self):
		"""Override description."""
		return "This is a custom control"
	
	def _get_value(self):
		"""Override value."""
		return self._getCustomValue()
	
	def _get_states(self):
		"""Override states."""
		states = super().states
		if self._isCustomSelected():
			states.add(controlTypes.State.SELECTED)
		return states

Handling Events

Overlay classes can handle events specific to their instances:
events.py
from NVDAObjects.IAccessible import IAccessible
import ui
import controlTypes

class CustomList(IAccessible):
	"""Custom list with enhanced event handling."""
	
	def event_gainFocus(self):
		"""Custom focus handling."""
		super().event_gainFocus()
		# Announce additional information
		count = self.childCount
		ui.message(f"{count} items in list")
	
	def event_selection(self):
		"""Handle selection changes."""
		super().event_selection()
		selectedCount = len(self.getSelectedChildren())
		ui.message(f"{selectedCount} items selected")
	
	def event_valueChange(self):
		"""Handle value changes."""
		# Announce in custom format
		ui.message(f"Value: {self.value}")

Event Methods in Objects

When events are defined on NVDA Objects (not app modules/global plugins), the signature is different:
# In overlay class: only takes self
def event_gainFocus(self):
    # No obj or nextHandler parameters
    super().event_gainFocus()

# In app module/global plugin: takes obj and nextHandler  
def event_gainFocus(self, obj, nextHandler):
    # obj is the NVDA Object
    nextHandler()

Adding Scripts

Add keyboard commands specific to certain controls:
scripts.py
from NVDAObjects.IAccessible import IAccessible
from scriptHandler import script
import ui
import controlTypes

class CustomDataGrid(IAccessible):
	"""Data grid with custom navigation commands."""
	
	@script(
		description="Move to next column",
		gesture="kb:control+rightArrow"
	)
	def script_nextColumn(self, gesture):
		"""Move to next column in grid."""
		nextCell = self._getNextColumn()
		if nextCell:
			nextCell.setFocus()
		else:
			ui.message("Last column")
	
	@script(
		description="Read column header",
		gesture="kb:NVDA+shift+c"
	)
	def script_readColumnHeader(self, gesture):
		"""Announce current column header."""
		header = self._getColumnHeader()
		if header:
			ui.message(header)
		else:
			ui.message("No header")
	
	def _getNextColumn(self):
		"""Get next column cell."""
		# Implementation
		return None
	
	def _getColumnHeader(self):
		"""Get column header text."""
		# Implementation
		return None

Initialization

Use initOverlayClass to initialize overlay classes:
initialization.py
from NVDAObjects.IAccessible import IAccessible
import controlTypes

class CustomControl(IAccessible):
	"""Control that needs initialization."""
	
	def initOverlayClass(self):
		"""Initialize overlay class."""
		# Called after the object is fully constructed
		self.customData = {}
		self.bindGestures({
			"kb:enter": "activate",
			"kb:space": "activate"
		})

Text Support

Implement text support for controls by providing a TextInfo class:
from NVDAObjects.IAccessible import IAccessible
import textInfos.offsets

class CustomControl(IAccessible):
	"""Control with custom text support."""
	
	TextInfo = CustomTextInfo

class CustomTextInfo(textInfos.offsets.OffsetsTextInfo):
	"""TextInfo for custom control."""
	
	def _getStoryText(self):
		"""Get all text content."""
		# Extract text from control
		return self.obj._getCustomText()
	
	def _getStoryLength(self):
		"""Get total length."""
		return len(self._getStoryText())
	
	def _getLineOffsets(self, offset):
		"""Get start and end offsets for line at offset."""
		text = self._getStoryText()
		start = text.rfind('\n', 0, offset) + 1
		end = text.find('\n', offset)
		if end == -1:
			end = len(text)
		return (start, end)

Behaviors

NVDA provides behavior mixins for common functionality:
behaviors.py
from NVDAObjects.behaviors import (
	EditableText,
	Dialog,
	RowWithFakeNavigation,
	KeyboardHandlerBasedTypedCharSupport
)
from NVDAObjects.IAccessible import IAccessible

class CustomEdit(EditableText, IAccessible):
	"""Editable text with caret tracking."""
	pass

class CustomDialog(Dialog, IAccessible):
	"""Dialog with special handling."""
	pass

class CustomRow(RowWithFakeNavigation, IAccessible):
	"""Table row with arrow key navigation."""
	pass

class CustomTerminal(KeyboardHandlerBasedTypedCharSupport, IAccessible):
	"""Terminal with typed character support."""
	pass

Common Behaviors

EditableText
mixin
Provides caret tracking and text editing support
Dialog
mixin
Dialog-specific functionality
ProgressBar
mixin
Progress bar announcement logic
RowWithFakeNavigation
mixin
Enables row/column navigation with arrow keys
KeyboardHandlerBasedTypedCharSupport
mixin
Reports typed characters in applications that don’t fire character events

Practical Examples

Example 1: Custom Button with Icon

customButton.py
from NVDAObjects.IAccessible import IAccessible
import controlTypes
import ui

class IconButton(IAccessible):
	"""Button that displays only an icon, needs text name."""
	
	def _get_name(self):
		"""Get name from tooltip or aria-label."""
		# Try aria-label first
		ariaLabel = self._getAriaLabel()
		if ariaLabel:
			return ariaLabel
		
		# Fall back to tooltip
		tooltip = self.description
		if tooltip:
			return tooltip
		
		# Last resort: use role
		return "Button"
	
	def _get_role(self):
		"""Ensure role is button."""
		return controlTypes.Role.BUTTON
	
	def _getAriaLabel(self):
		"""Extract aria-label attribute."""
		# Implementation depends on control type
		return None

Example 2: Enhanced List Item

enhancedList.py
from NVDAObjects.IAccessible import IAccessible
import controlTypes
import ui

class EnhancedListItem(IAccessible):
	"""List item with rich content."""
	
	def _get_name(self):
		"""Construct name from multiple elements."""
		parts = []
		
		# Get main text
		mainText = self._getMainText()
		if mainText:
			parts.append(mainText)
		
		# Get secondary info
		secondary = self._getSecondaryInfo()
		if secondary:
			parts.append(secondary)
		
		# Get badge/status
		status = self._getStatus()
		if status:
			parts.append(f"Status: {status}")
		
		return ", ".join(parts)
	
	def _get_positionInfo(self):
		"""Provide position in list."""
		parent = self.parent
		if not parent:
			return None
		
		index = self._getIndex()
		total = parent.childCount
		
		return {
			'indexInGroup': index,
			'similarItemsInGroup': total
		}
	
	def _getMainText(self):
		"""Get main text from control."""
		return super().name
	
	def _getSecondaryInfo(self):
		"""Get secondary information."""
		# Extract from child elements
		return None
	
	def _getStatus(self):
		"""Get status indicator."""
		return None
	
	def _getIndex(self):
		"""Get item index."""
		return 1

Example 3: Custom Tree Item

customTree.py
from NVDAObjects.IAccessible import IAccessible
from scriptHandler import script
import controlTypes
import ui

class CustomTreeItem(IAccessible):
	"""Tree item with custom navigation."""
	
	def _get_positionInfo(self):
		"""Position info with level."""
		info = super().positionInfo or {}
		info['level'] = self._getLevel()
		return info
	
	def _getLevel(self):
		"""Calculate tree level."""
		level = 1
		parent = self.parent
		while parent and parent.role == controlTypes.Role.TREEVIEWITEM:
			level += 1
			parent = parent.parent
		return level
	
	@script(
		description="Expand or collapse tree item",
		gesture="kb:space"
	)
	def script_toggleExpand(self, gesture):
		"""Toggle expanded state."""
		if controlTypes.State.EXPANDED in self.states:
			self._collapse()
			ui.message("Collapsed")
		else:
			self._expand()
			ui.message("Expanded")
	
	def _expand(self):
		"""Expand tree item."""
		self.doAction()
	
	def _collapse(self):
		"""Collapse tree item."""
		self.doAction()

Example 4: Custom Data Table

dataTable.py
from NVDAObjects.IAccessible import IAccessible
from scriptHandler import script
import controlTypes
import ui

class DataTableCell(IAccessible):
	"""Table cell with row/column headers."""
	
	def _get_name(self):
		"""Include row and column headers."""
		parts = []
		
		# Row header
		rowHeader = self._getRowHeader()
		if rowHeader:
			parts.append(rowHeader)
		
		# Column header
		colHeader = self._getColumnHeader()
		if colHeader:
			parts.append(colHeader)
		
		# Cell value
		value = super().name
		if value:
			parts.append(value)
		
		return ", ".join(parts)
	
	@script(
		description="Read row header",
		gesture="kb:NVDA+shift+r"
	)
	def script_readRowHeader(self, gesture):
		"""Announce row header."""
		header = self._getRowHeader()
		if header:
			ui.message(f"Row: {header}")
		else:
			ui.message("No row header")
	
	@script(
		description="Read column header",
		gesture="kb:NVDA+shift+c"
	)
	def script_readColumnHeader(self, gesture):
		"""Announce column header."""
		header = self._getColumnHeader()
		if header:
			ui.message(f"Column: {header}")
		else:
			ui.message("No column header")
	
	def _getRowHeader(self):
		"""Get row header text."""
		# Implementation specific to table structure
		return None
	
	def _getColumnHeader(self):
		"""Get column header text."""
		# Implementation specific to table structure
		return None

Caching Properties

Use _cache dictionary for expensive computations:
caching.py
from NVDAObjects.IAccessible import IAccessible

class ExpensiveControl(IAccessible):
	"""Control with expensive property calculations."""
	
	def _get_name(self):
		"""Name with caching."""
		try:
			return self._cache['name']
		except KeyError:
			name = self._calculateExpensiveName()
			self._cache['name'] = name
			return name
	
	def _calculateExpensiveName(self):
		"""Expensive name calculation."""
		# Complex processing
		return "Calculated name"
	
	def event_nameChange(self):
		"""Clear cache when name changes."""
		if 'name' in self._cache:
			del self._cache['name']
		super().event_nameChange()

API-Specific Base Classes

Different accessibility APIs require different base classes:
apiClasses.py
# IAccessible/MSAA objects
from NVDAObjects.IAccessible import IAccessible
class MyIAccessibleControl(IAccessible):
    pass

# UI Automation objects
from NVDAObjects.UIA import UIA
class MyUIAControl(UIA):
    pass

# Java Access Bridge objects
from NVDAObjects.JAB import JAB
class MyJABControl(JAB):
    pass

# Window objects (direct Windows API)
from NVDAObjects.window import Window
class MyWindowControl(Window):
    pass

Testing

1

Create app module or global plugin

Implement chooseNVDAObjectOverlayClasses method.
2

Define overlay classes

Create your custom NVDA Object classes.
3

Place in scratchpad

Put files in appropriate scratchpad directory.
4

Enable NVDA speech viewer

Tools > Speech Viewer to see what NVDA announces.
5

Test with object navigation

Use NVDA object navigation to examine objects.

Debugging

# Press NVDA+F1 on any object to see developer info
# Or use script:
from scriptHandler import script
import ui
import api

@script(gesture="kb:NVDA+shift+d")
def script_debugInfo(self, gesture):
    obj = api.getNavigatorObject()
    info = []
    info.append(f"Name: {obj.name}")
    info.append(f"Role: {obj.role}")
    info.append(f"Class: {obj.__class__.__name__}")
    info.append(f"WindowClass: {obj.windowClassName}")
    ui.message(", ".join(info))

Best Practices

Performance:
  • Cache expensive computations
  • Don’t perform long operations in property getters
  • Clear caches appropriately when properties change
  • Use _cache dictionary for caching
Code Quality:
  • Always call super() methods unless overriding completely
  • Provide meaningful names and descriptions
  • Test with multiple applications
  • Handle missing/None values gracefully
  • Document complex logic

Common Patterns

patterns.py
# Pattern 1: Name from multiple sources
def _get_name(self):
    return (
        self._getAriaLabel() or
        self._getTooltip() or
        super().name or
        "Unlabeled control"
    )

# Pattern 2: Add to existing states
def _get_states(self):
    states = super().states
    if self._isCustomCondition():
        states.add(controlTypes.State.CHECKED)
    return states

# Pattern 3: Format position info
def _get_positionInfo(self):
    info = super().positionInfo or {}
    info.update({
        'indexInGroup': self._getIndex(),
        'similarItemsInGroup': self._getTotal()
    })
    return info

# Pattern 4: Combine roles
def _get_role(self):
    baseRole = super().role
    if self._hasSpecialBehavior():
        return controlTypes.Role.BUTTON
    return baseRole

Build docs developers (and LLMs) love