Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nvaccess/nvda/llms.txt

Use this file to discover all available pages before exploring further.

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