Exploring Slash Commands Accessibility

What we’ll be doing

Exploring the accessibility of various “slash commands” or “suggestion” implementations in rich text editors and plain text editors.

Foreword

This is a first pass document. There’s very little editing / proofreading. Read at your own risk.

I am not an accessibility professional. If accessibility is paramount, please consult accessibility experts. I’m just someone who knows just enough to be dangerous.

This isn’t meant to attack any companies or implementations. This is for learning purposes.

Due to time constraints, for this article I’ll be using VoiceOver on MacOS and Firefox v125.

Part of the reason for not using Safari is because Firefox + VoiceOver has always been good for me when using Comboboxes / Menus which is what we’ll be exploring. Safari has some odd behavior with comboboxes.

Most users use NVDA / JAWS with Windows + Chrome. That is the ideal testing environment for accessibility testing, but this article is less about the actual readings, and more about the actual structure of the slash commands.

I won’t be pasting the full HTML, but rather small snippets of the general pattern with relevant pieces. Pasted HTML will be from when the suggestions are in the “expanded” state.

Getting started

There’s a few implementations of “slash commands” I can think of.

So, let’s see what they do!

GitHub

GitHub has implemented “slash commands”, also known as “suggestions” if you’re familiar with ProseMirror Suggestion API for things like mentions.

Holy crap GitHub! This was so hard to get your HTML. Anytime I clicked in the devTools, the suggestions disappeared. I had to use a setTimeout(() => alert("YO"), 5000) and then open the suggestions to freeze it to be able to inspect the HTML. Well played.

HTML
<textarea
  role="combobox"
  aria-controls="<wrapper-around-listbox-below>"
  aria-activedescendant="slash-command-item-code"
  aria-haspopup="listbox"
  aria-autocomplete="list"
></textarea>

<ul role="listbox">
  <li role="option" aria-selected="true">
    <form></form>
    <!-- other stuff -->
  </li>
</ul>

Now this is what I expect! If you maintain focus on the editor while showing commands, it should be a combobox. I would think!

What’s interesting to note is that the <textarea> does not have a role="combobox" until it has a suggestion to show. So it dynamically adds the role. Don’t know if the screenreader is going to like that, but we shall see!

Regardless, this is largely what I would’ve expected / done.

How does GitHub suggestions read

Survey says it reads pretty well! It reads the currently selected option, tells you how many options there are, it tells you that you’re within a listbox. What’s interesting is if you focus the outer <textarea> is still reads it as a <textarea> and not a combobox, but I would argue that’s minor.

Good stuff GitHub!

Bonus rant about GitHub form in a form

Points deducted because if you look closesly enough, that <form> within the <li> for the suggestions is actually wrapped by a larger <form>, so there’s actually nested forms in the “Issue” “slash command” suggestion, but beyond that, the HTML is quite clean.

Here’s a quick little peek in the console:

JavaScript
temp1
// => <form class="js-slash-command-suggestion-form" data-turbo="false" action="/KonnorRogers/personal-site/slash_apps/e96c27a140fd4d56fbfeac0c1d8919bf1351ccba1b6b6e8459d36f48db9ba90d/code?surface=issue_body" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="YNOHTAZdeBduRkq56uGxHN9jV-fJuqttlnvBID018LZeuDBycPf21fL2jjayNOGGcv4xUlBgcD27W6b8J8E-Ew" autocomplete="off"></form>

temp1.closest("form")
// => <form class="js-slash-command-suggestion-form" data-turbo="false" action="/KonnorRogers/personal-s…code?surface=issue_body" accept-charset="UTF-8" method="post">

Discord

First off, Discord apparently calls preventDefault() when doing CMD+Option+i, so it was really annoying to open devTools.

Anyways, here’s the general structure of Discord’s “slash commands”:

HTML
<!-- This is the rich text editor -->
<div
  role="textbox"
  aria-multiline="true"
  aria-haspopup="listbox"
  aria-autocomplete="list"
  contenteditable="true"
  aria-expanded="true"
  aria-controls="uid_269"
></div>

<-- This is the autocomplete -->
<div role="listbox">
  <ul role="group">
    <div role="option" tabindex="-1">
      <!-- more divs than I can count -->
    </div>
  </ul>
</div>

Now we can argue about the semantics of the HTML. I’ll leave that for you the reader, to scratch your head.

Anyways, it’s interesting to see they went with the listbox approach. Now what’s interesting is that the textbox is setup like a combobox by having aria-haspoup="listbox", but it doesn’t utilize aria-activedescendant. Which means they could use a “roving tabindex” approach on the listbox and it should work, but it requires shifting focus off of the textbox, which when testing, it does not do. It keeps focus on the textbox and uses “virtual focus” on the commands by using aria-selected.

So let’s move on and see how it reads.

Discord usage with VoiceOver

Discord froze VoiceOver whenever I tried to use slash commands. In fact, Discord froze my whole browser ever just attempting to open dev tools. And when it didn’t freeze, it auto closed the opened menu. So I wasn’t able to get any good information about how VoiceOver + Firefox handles it.

Slack

Alright, on to Slack’s chat slash commands!

HTML
<div
  contenteditable="true"
  role="textbox"
  tabindex="0"
  aria-multiline="true"
  spellcheck="true"
  aria-activedescendant="tab_complete_ui_item_0"
></div>

<ul role="listbox">
  <li role="option" tabindex="-1"></li>
</ul>

Nothing flashy with the listbox. Looks nice. I like it. If you notice in GitHub and in Slack, the options have tabindex="-1", this allows screenreaders to focus the item which allows it to read the “option”.

(Thank you to DiegoHaz creator of AriaKit for telling me about this)

Surprising things that are missing from the textbox:

I’m surprised these are missing, but maybe because its not technically a role="combobox" it may not be needed?

Let’s see how it reads.

How Slack reads suggestions

Unfortunately, whenever VoiceOver would “focus” an option in the suggestions, it would immediately cause the suggestions menu to close. I was unable to get any information on how suggestions got read.

TinyMCE

This isn’t the actual TinyMCE, but rather the CodePen from this blog post because I couldn’t find a proper example on their site.

https://www.tiny.cloud/blog/slash-commands-rich-text-editor/

https://codepen.io/tinymce/pen/poebmjP

So let’s start with the HTML:

HTML
<iframe>
  <body
    contenteditable="true"
    aria-activedescendant="menu-item_1354807282151713924642819"
    aria-owns="autocompleter_6731546931041713924532187"
  >
  </body>
</iframe>

<div role="menu">
  <div role="menuitem"></div>
</div>

Again, no aria-expanded, aria-haspopup="listbox", aria-autocomplete="list". But it does have aria-activedescendant.

Interesting to note they are using a menu, but the menu is in the host document, not in the <iframe>.

I would expect the menu to grab focus from the editor, but also, again, we’ll see. You never know until you try.

Running the TinyMCE slash commands through the screenreader

Interestingly, it reads pretty well. It tells you you’re on a menu, it doesn’t seem to steal focus from the main text editor, although I can’t find any aria-selected on the menu items, nothing tells you it’s selected. I suspect TinyMCE quickly focuses the menu item, then swaps focus back to the editor. This works well on my laptop, but could possibly be jarring on a phone where it may cause the virtual keyboard to quickly disappear and reappear if the user is using VoiceOver on their phone.

(Thank you to Devon Govett for pointing that out to me when talking about Comboboxes on Twitter with me)

Notion

Again, HTML first.

HTML
<div
  contenteditable="true"
>
</div>

<div id=":r14:" tabindex="0" role="menu" aria-activedescendant=":r15:">
  <div role="menuitem" tabindex="-1" id=":r15:"></div>
</div>

No aria-haspopup, aria-activedescendant, aria-expanded, aria-autocomplete, role="textbox".

I don’t have high hopes for this. Can <idref>s even start with a :? I don’t even know. Again, like TinyMCE, it uses a role="menu", I imagine it’ll do like TinyMCE and leave focus on the editor and use the quick swap focus trick to get menu items to read.

Let us find out.

How Notion reads suggestions

The cursor stays on the editor and doesn’t leave. When I use arrow keys to “visually select” a suggestion, nothing gets read. I don’t even get read the text. It doesn’t tell me I’m on a menu. Doesn’t tell me I have a menuitem currently focused. In fact, when I enter a contenteditable text block on Notion, it just tells me I’m within a group. It doesn’t tell me I’m in a textbox, input, nothing. There is no feedback.

Conclusion

GitHub and TinyMCE seemed to be the only ones to have usable “suggestions” or “slash commands”. It was impossible to evaluate Slack’s / Discord’s. Slack’s seemed pretty similar to GitHub’s in terms of structure.

If I were to build suggestions, it seems like the most reliable way is to use role="combobox". I’ll probably messing around in the future with different permutations, but this seems the most solid and expected behavior.

As always, test with an actual screenreader, test with different screenreaders + browser combos (which I didn’t do here, because my free time is finite), hire accessibility professionals to audit this stuff for you, involve your accessbility team if you have one, etc.

That’s all I got. Best of luck on your suggestions / slash commands! I hope you learned something, I know I did!

Additional notes

Editor note 1 04/23/2024

When writing this I totally forgot about navigating options via ctrl+option+arrowDown and ctrl+option+arrowUp in VoiceOver which is an intended way of navigating combobox options in VoiceOver.

Using ctrl+option+arrow keys on Github causes it to shift focus back to the editor and off of the listbox not allowing you to select any suggestions.

In TinyMCE, ctrl+option+arrow stays on current menuitem does not shift selection.

Discord and Slack obviously do nothing since I can’t get them working in VoiceOver.