Can Tab cycle through 3 modes (e,g, object, edit, pose)

With an Armature, if I’m in Object or Edit mode, the tab key cycles between just those two. If I select Pose (using the mouse to access the menu), tab then cycles between Pose and Edit. Is there a way for tab to cycle through all three?

I’ve also found Ctrl-tab which does this: From Object/Pose mode, it switches between just them. In Edit mode it brings up the pie menu which shows all 3 modes. If I select object or pose the behaviour reverts back to switching between just them…

I’ve seen the option for ‘Tab for Pie Menu’ in preferences > keymap but I’m not keen on that. Maybe there’s something in the key options below but I could not find anything that looked like what I was after.

Cycling is annoying, so, no.

That said, you could certainly script an operator that will do it and set replace the Tab key in keyboard preferences to trigger your operator instead of the usual built-in one.

UPDATED to VERSION 1.1

All right, so, doing this is non-obvious (and it struck my interest), so… here’s an add-on that makes the Tab key cycle through Object → Pose → Edit → Object modes when the active object is an armature.
(You can still use Ctrl+Tab to get back to Object mode without passing through Edit mode.)


First, save the following script somewhere convenient:

tab-3.py

# SPDX-License-Identifier: BSL-1.0
# 2026 Michael Thomas Greer

bl_info = {
	"name"        : "Tab-3",
	"description" : "Tab through Object, Pose, and Edit Modes",
	"author"      : "Michael Thomas Greer (Dúthomhas)",
	"version"     : (1, 1),
	"blender"     : (3, 0, 0),
	"location"    : "Preferences > Add-Ons > Tab-3",
	"warning"     : '',
	"doc_url"     : '',
	"support"     : "COMMUNITY",
	"category"    : "Interface",  # IDK, would "Object" be more apropos?
}

import bpy


# This add-on overrides the default Blender key binding:
#
#   3D View > Object Non-modal > Set Object Mode
#
# with one that notices if the active object is an Armature and cycles through:
#
#   Object Mode --> Pose Mode --> Edit Mode --> Object Mode
#
# If the object is not an Armature, the default toggle Edit mode operator
# is invoked instead.
#
#
# Observe that Shift-Tab is NOT modified -- that is the shortcut for Snap mode!


keymaps = []


class INTERFACE_OT_tab3(bpy.types.Operator):
	bl_idname = "interface.tab3"
	bl_label  = bl_info["name"]
	bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}

	def execute(self, context):
		try:
			if context.active_object.type == 'ARMATURE':
				try:
					modes = ('POSE', 'EDIT', 'OBJECT')
					n = modes.index(bpy.context.object.mode)
					return bpy.ops.object.mode_set(mode=modes[(n+1)%3])

				except (ValueError, RuntimeError):
					return bpy.ops.object.mode_set(mode='OBJECT')

			return bpy.ops.object.editmode_toggle()

		except (AttributeError, RuntimeError):
			return {'FINISHED'}


def register():
	bpy.utils.register_class(INTERFACE_OT_tab3)
	wm = bpy.context.window_manager
	km = wm.keyconfigs.addon.keymaps.new(name='Object Non-modal', space_type='EMPTY') # huh, not 'VIEW_3D'
	kmi = km.keymap_items.new(INTERFACE_OT_tab3.bl_idname, 'TAB', 'PRESS')
	keymaps.append((km, kmi))


def unregister():
	for km, kmi in keymaps:
		km.keymap_items.remove(kmi)
	keymaps.clear()
	bpy.utils.unregister_class(INTERFACE_OT_tab3)


if __name__ == "__main__":
	register()



Then install it the usual way you install add-ons in Blender: Edit > Preferences > Add-ons. (These days you also have to click the little ⌄ button to get the “Install from disk” menu option.) Navigate to wherever you saved “tab-3.py” and select it.

The add-on should install and activate automatically.


Disabling and uninstalling also works the usual way.

I made no effort to disable Blender’s default Tab keybinding — there shouldn’t be any need to do that (add-on keybindings should override default bindings), but let me know if I’m mistaken. (I only tested on 3.0 and 4.5. IDR whether I tested against 5+, but it should not differ from 4.5.)



Update: version 1.1

Sorry, I guess this is the consequence of just throwing something together and letting everyone else do the testing.

I repented and spent some time trying to trip this new version up, but I am sure that I couldn’t possibly have thought of everything someone may try to do to it, so if you manage to get it to barf, let me know.

Known Behaviors:

  • Linked armature assets with a library override are supposed to complain if you try to switch to Edit mode. You will continue to see the following error message in the console if you try:

    Error: Unable to execute 'Edit Mode', error changing modes

    The popup error message is suppressed, though. It is unclear to me whether or not I should let it appear, as the behavior of Tab-3 is meant to simply cycle through acceptable modes.
    (You can still explicitly select Edit mode using the drop down menu in the header bar to produce the popup.)

    The message in the console cannot be eliminated just by trapping exceptions, but it can be elimated, if desired.

    My current thought is that if Edit mode is not available then in this instance the user does not want to get a popup message, but that there should at least be an explanation in the console for why it didn’t switch to Edit mode.

    Let me know if you think I am incorrect in either assessment. Otherwise I’ll call this behavior as “close enough”.

Works also on 3.6 :wink:

Wow, thanks for that effort. The script/addon works nicely. I made a few tweaks to reduce some unpleasant looking error messages if I’m clumsy and press tab with an invalid selection, or none:

	invalid_types = {'CAMERA', 'LIGHT', 'VOLUME', 'EMPTY'} # not an exhaustive set, but enough for me
	
	def execute(self, context):
		if context.active_object != None:
			if context.active_object.type == 'ARMATURE':
				mode = bpy.context.object.mode
				modes = ('POSE', 'EDIT', 'OBJECT')
				for n in range(3):
					if mode == modes[n]:
						return bpy.ops.object.mode_set(mode=modes[(n+1)%3])
				return {'FINISHED'}
			elif context.active_object.type not in self.invalid_types:
				return bpy.ops.object.editmode_toggle()
		return {'FINISHED'}

(followed by the same register/unregister code from your version)

Not sure of the importance of the first “return {‘FINISHED’}” statement in the edited version. Keeping it in for now but I may be taking a cargo cult approach with that.

You are right, I should have considered that. I didn’t initially consider this more than a toy. Sorry to have not taken your problem seriously.

I updated the add-on to handle all the errors I could think of. Let me know if I missed anything. Be sure to review the behavioral considerations listed at the end of my last post and let me know if you think I have it right or not.

@R_Soul @Okidoki @SterlingRoth

Thanks again for your time and effort. The revision works nicely for me, and is both neater and more thorough than my own edit.

No need to apologise. I wouldn’t have had the patience to come up with that code. I’d have spent lots of time starting at the keymap options and posting a feature request in the development forum with little hope of it being implemented.

As for testing etc, this is meant to be a collaborative endeavour so I think the system worked as intended.

I wonder if there is something seriously wrong with returning another operator from an operator. :laughing: I haven’t really seen it done and what operators usually return is very specific:

Operators don’t have return values as you might expect, instead they return a set() which is made up of: {'RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH'}. Common return values are {'FINISHED'} and {'CANCELLED'}, the latter meaning that the operator execution was aborted without making any changes or saving an undo history entry.

I wouldn’t do

return bpy.ops.object.editmode_toggle()

But I am not sure why that is actually wrong. Maybe it could fail somehow when used with another modal operator running or if something wrong happens with the returned operator’s execution. I don’t think it’s a good practice though. Maybe I don’t know something…

Well.. when usually using some typed computer language and also object oriented it sometimes feels weird when in python often “some string” is returned.. and you have to dig your way through the documentation or even the source to find out what this is supposed to do at all.. ( also the “usual” API- dcumentation only (!) documents the API and not the idea and principles behind it.. even if there are some “commonly used names” for somthing like: addListener for some “observer”-design pattern ← see the diff ? :wink:)

It’s not a naming convention. That’s very different. It’s expected by the API that operators return those specific values. For what reason? I have no idea. But I would expect there is some reason and things might break in unexpected ways in some circumstances. :man_shrugging: It would be interesting to know how. :laughing: I still think it’s just not good practice.

Well.. in this special case i guess this means:

  • FINISHED: everything was fine.. put this into the undo history
  • CANCELLED: something went wrong no undo but maybe redo history
  • RUNNIG_MODAL: stop interacting with other GUI
  • PASS_THROUGH: hmm.. :thinking: i have to pass here

Operators are not normal functions. They are an interface.

In CS parlance, the Tab-3 operator is what you would call a trampoline — it is injected between a keymap and the operator(s) that would normally be executed by the keymap.

Were we trampolining the interface of a more complicated operator (like a modal one), we would have to implement that operator’s full interface as well, and bounce the communications between the caller and the other operator through our interface.

Fortunately for us Tab-3 is only injected between the keymap and two non-modal operators, for which the full interface is just an “execute” method. (And maybe a “poll” method, which I freely ignored.)

The only correct responses from the non-modal operators that Tab-3 executes are in the set {'FINISHED', 'CANCELLED', 'ERROR'}. Tab-3’s reported termination state is therefore the termination state of the chosen operator’s execution. This is correct, as we are returning to the caller (the keymap) the status of the target operator, not that of the trampoline.

Hope this helps.

I’m not seeing anything wrong with this

return bpy.ops.object.editmode_toggle()

Basically, the return statement executes the operator bpy.ops.object.editmode_toggle() and returns whatever value the operator returns. It’s the same thing as

result = bpy.ops.object.editmode_toggle()
return result

I’m no python expert, but that’s my take on it.

Randy

Afterthought -

Back in the day - Commodore 64 - that was called a wedge, I think. You could replace part of the C64’s OS that handled keystrokes with an instruction to jump to a different memory address where you would have code to check for certain keystrokes. If keystroke found, then do something, else return execution to the other memory location. That was done using Assembly Language…

Yeah. Interesting. Maybe it’s OK. :thinking: Maybe that was just wrong intuition of mine.

Yeah, it gets a lot of names. I keep wanting to call it a thunk, but the most common term these days is trampoline, which itself is an overloaded term, lol. Like window.

Whatever the term, the idea is as old as computing, and it pops up in so many different use cases.

Commodore 64

My first programming was on a Commodore too! But just simple things. I had no access to a tape or disk drive so anything I typed in would be lost at next power cycle, so I never put much effort in to it. My dad just bought it so us kids could play games (and stop hogging his Z80 for gaming).

Got really interested on the IBM PC 8088 my dad eventually bought for the family.

Since the GW-BASIC interpreter was too stupid to handle the HGC’s “hi-res” graphics mode, and later the same for a VGA card, I wrote assembly routines to do all the dirty work and linked them in to the interpreter so I could do cool video game graphics in Mode X, heh.

(At which my dad discovered me doing it that way and bought me a copy of Turbo Pascal 4, lol.)

Then we got a Trident Super VGA and I got my hot little hands on a copy of Ferraro’s book and there was no limit.

Too bad I’ve lost all that code. (I need about a thousand dollars of unencumbered money to recover the hard drive if I ever want it back.) I suppose I could just rewrite it, but there really isn’t any point these days. I’m sitting on a really nice system right now that doesn’t need any of that to write a game. I’ll just use Godot for the fun stuff. (And Blender, of course, for the other fun stuff.)

But I guess I can’t escape the urge to code here and there.