category: framework-deep-dive-ui menu-title: Focus tracking
Focus is where all the keyboard interactions (events) take place. It is an essential concept for any piece of a web page operated with a keyboard, be it a physical keyboard of your laptop or a software keyboard of your smartphone. And CKEditor 5 being a rich text editor is no exception here.
Every time you click a text field or an editor, it automatically prepares to accept your text. It is no longer just a static container for letters but something you can interact with using your keyboard. And you know it because you can see the familiar blinking caret somewhere inside of it. This subtle action called a focus change takes place hundreds of times every day as you navigate web pages, type your search queries, chat with your friends and fill in checkout forms when online shopping.
{@img assets/img/framework-deep-dive-focus-form-example.gif 578 The animation showing the focus moving from one field to another when filling in the form.}
Focusing text fields feels so natural we usually do not give it much thought. But if it wasn't for that simple action... there would be no way to type text. Where would it go if there was no focused field? Focus informs the software about your intentions and it is synonymous with the context.
But CKEditor is more than a simple text field. Yes, it has the main space where you type your text but other places also allow you to type, for instance, a link URL field or a form with plenty of inputs allowing you to configure the look of a table.
{@img assets/img/framework-deep-dive-focus-link-blinking-caret.gif 606 The animation showing the focused link URL input in CKEditor 5 with a blinking caret.}
And when many places accept focus, there must be some systems out there to discover and manage it. These systems work, for instance, so you will not find yourself in a situation where CKEditor 5 loses focus (or gets blurred) and you cannot type your text. Or they make sure you can see and use the editor toolbar as long as you keep editing. These are just a few examples of why focus management systems are necessary. Now it should also be clear that they are essential for the editing experience.
In the following chapters of this guide, we will explain how these systems work, what makes CKEditor "focusable", and how to develop new editor features so they fit into the existing focus management systems and patterns:
Keep in mind that information in this chapter concerns the editor **engine layer only**. To learn about focus in the user interface, check out the [next section](#focus-in-the-editor-ui) of this guide.
The main editable area of CKEditor 5 WYSIWYG editor can be focused thanks to the contenteditable DOM attribute. This attribute tells the web browser that a web page element can be edited like any other text field, which also means it must be able to receive focus.
Each root of the editing view has the contenteditable attribute. The editing view uses the {@link module:engine/view/observer/focusobserver~FocusObserver FocusObserver} (learn more about {@link framework/guides/architecture/editing-engine#observers view observers}) to track focus in editables by listening to native DOM focus and blur events coming from them.
Already confused? Take a look at our {@link framework/guides/architecture/editing-engine#observers Editing engine} guide that explains what the editing view, editables and other building blocks of CKEditor 5 are.
You can check if the view document is focused using its {@link framework/guides/deep-dive/observables observable} {@link module:engine/view/document~Document#isFocused} property. Create an editor instance and execute the following code:
console.log( editor.editing.view.document.isFocused );
If you run this snippet from the web browser's developer console, it should return false unless you managed to keep the editor focused (e.g. by running a debugger and freezing the DOM). This happens because the editor loses focus the moment you switch to developer tools to execute the snippet.
So how do I know isFocused actually works? Since it is observable you can see how it changes live:
editor.editing.view.document.on( 'change:isFocused', ( evt, data, isFocused ) => {
console.log( `View document is focused: ${ isFocused }.` );
} );
Click the editable area of the editor and then click somewhere else — the isFocused property will change its value when you do that. The same will also happen if you run an editor with {@link framework/guides/custom-editor-creator multiple editing roots} and navigate across them.
To spice things up even more, you should also know isFocused will change when you focus any {@link framework/guides/tutorials/implementing-a-block-widget nested editable} in the content (take, for example, a {@link features/image#image-captions caption of an image}). Sounds weird, right? This is because every nested editable in the content has the contenteditable attribute, too, and for the web browser moving your caret inside it means the main editable element is blurred and the nested one is focused.
The simplest way to focus the editor is to call the {@link module:core/editor/editor~Editor#focus editor.focus()} method.
However, you may wish to explicitly focus the editable area of CKEditor 5 when a certain action is executed (e.g. a button is clicked). To do that, use the {@link module:engine/view/view~View#focus focus()} method of the editing view:
editor.editing.view.focus();
This snippet focuses the editable that has the selection. If the editor has not been focused yet, this will focus the very first editable. If an editor has multiple editing roots and the user was editing content, focus will be brought back where the user left off.
Focusing the editor does not change its selection. If you want to focus the editor and move the caret to a specific position, you should call `editor.editing.view.focus()` first and then use the {@link module:engine/model/writer~Writer#setSelection `setSelection()`} method of the {@link framework/guides/architecture/editing-engine#model model writer} to change the selection.
If you read the previous section of this guide you should know that there is already a layer responsible for tracking focus implemented in the {@link framework/guides/architecture/editing-engine editor engine}. But because the {@link framework/guides/architecture/intro editor framework is modular}, this layer is only concerned by the focus in editable roots of the editor and it knows nothing of the user interface. This granularity makes it possible, for instance, to create a fully–functioning editor without the UI.
As for the user interface of CKEditor 5, it is a composition of multiple components {@link framework/guides/architecture/ui-library#view-collections-and-the-ui-tree organized as a tree}. This tree determines not only the logical structure of the UI (a toolbar has a dropdown, a dropdown has a button, a button has an icon, etc.) but also its behavior, and that includes tracking and maintaining focus as the user navigates and interacts with various pieces of the interface.
Feeling overwhelmed? Take a look at the {@link framework/guides/architecture/ui-library UI library} guide and learn some basics about how the UI of CKEditor 5 works and what its main building blocks are.
To sum up, there are two main reasons why focus is being tracked separately on the UI level:
Focus management lives next to the {@link framework/guides/architecture/ui-library#view-collections-and-the-ui-tree user interface element tree} and its architecture is also based on components that respond to user actions within its boundaries and their children. Take a look at a common keyboard navigation scenario in a {@link examples/builds/classic-editor classic editor} instance:
{@img assets/img/framework-deep-dive-focus-toolbar-nav.gif 950 The animation showing the focus moving as the user navigates to the heading drop–down in the toolbar.}
Here are the focus layers that play a role in the navigation and a brief overview of what happens at each layer:
{@img assets/img/framework-deep-dive-focus-toolbar-nav-layers.png 1019 The image showing the focus layers used during navigation.}
editor.ui.focusTracker}).focus() method is executed. That brings us to the third layer.
ToolbarView, also needs a focus tracker and a focus cycler to allow smooth keyboard navigation across its items.focus()} method that focuses its element in the DOM.And here is the summary of the tools used by each focus layer (UI component):
| Focus layer | Needs focus tracker? | Needs keystroke handler? | Needs focus cycler? | |
|---|---|---|---|---|
| 1 | {@link module:editor-classic/classiceditorui~ClassicEditorUI} | ✅ | ✅ | ❌ |
| 2 | {@link module:ui/toolbar/toolbarview~ToolbarView} | ✅ | ✅ | ✅ |
| 3 | {@link module:ui/dropdown/dropdownview~DropdownView} | ❌ | ✅ | ❌ |
| 4 | {@link module:ui/list/listview~ListView} | ✅ | ✅ | ✅ |
| 5 | {@link module:ui/list/listitemview~ListItemView} | ❌ | ❌ | ❌ |
Most components have focus trackers to keep up with the focus inside of them. Some components that host more children also use focus cyclers and keystroke handlers to help the user navigate across them. You can learn how to use them in the later sections of this guide:
FocusTracker class,KeystrokeHandler class,FocusCycler class.Any UI {@link framework/guides/architecture/ui-library#views view} can be focusable. To become one, a view must implement the focus() method that focuses the DOM {@link module:ui/view~View#element element} and the tabindex="-1" attribute set on the element that prevents the native navigation using the keyboard (which should be handled by the focus cycler on the parent–level):
The [`tabindex="-1"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) is a native DOM attribute that controls whether a DOM element can be focused (and in which order). Setting its value to `"-1"` tells the web browser that it should exclude it from the native keyboard navigation and allow it only to be focused using JavaScript. Because your component will belong to the editor focus management system, you should do that if you want to avoid collisions with the web browser.
import View from '@ckeditor/ckeditor5-ui/src/view';
class MyListItemView extends View {
constructor( locale, text ) {
super( locale );
// ...
this.setTemplate( {
tag: 'li',
attributes: {
tabindex: -1
},
children: [ text ]
} );
}
// ...
focus() {
this.element.focus();
}
}
If a view has many focusable children (e.g. a list), the focus() method should focus the first child:
import View from '@ckeditor/ckeditor5-ui/src/view';
class MyListView extends View {
constructor( locale ) {
super( locale );
// ...
this.items = this.createCollection();
this.setTemplate( {
tag: 'ul',
children: this.items
} );
}
// ...
focus() {
if ( this.items.length ) {
// This will call MyListItemView#focus().
this.items.first.focus();
}
}
}
Focusable views are what make it possible to navigate the interface of CKEditor using the keyboard. In the next section, you will learn how parent views keep track of focus among their children using the FocusTracker class.
FocusTracker classOne of the key focus management helpers in the CKEditor 5 UI is the {@link module:utils/focustracker~FocusTracker FocusTracker} class and its instances. They work at the DOM level and offer a fairly simple API:
add()} and {@link module:utils/focustracker~FocusTracker#remove remove()} tracked DOM elements.isFocused} property telling the world that one of the tracked elements has focus in the DOM.focusedElement} property that precisely says which DOM element is focused.Focus trackers listen to DOM focus and blur events coming from elements they track and they determine if any is currently focused. As a rule of thumb, a {@link framework/guides/architecture/ui-library#view-collections-and-the-ui-tree parent} to more than one focusable element should have a focus tracker.
Keep in mind that simple components that have no focusable children or just a single focusable child may not need a focus tracker.
Each editor instance has a global focus tracker that can be accessed via {@link module:core/editor/editorui~EditorUI#focusTracker editor.ui.focusTracker}. It is a special instance that glues all the pieces of the user interface together (including the editing root) and stores the focus state of the entire editor instance.
You can always listen to the global focus tracker and tell if the user is using the UI:
editor.ui.focusTracker.on( 'change:isFocused', ( evt, data, isFocused ) => {
console.log( `The editor is focused: ${ isFocused }.` );
} );
The class responsible for the UI of the editor ({@link module:editor-classic/classiceditorui~ClassicEditorUI}, {@link module:editor-inline/inlineeditorui~InlineEditorUI}, etc.) must inform the global focus tracker about the focusable building blocks of the interface, for instance, editable roots of the editor (where the editing takes place) or toolbars.
The same applies to plugins bringing any focusable UI. For instance, the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} plugin that offers a shared floating balloon used by many features across the project (link editing, image tools, widget toolbars, etc.) also registers the balloon in the global focus tracker. Please keep this in mind when developing your custom editor plugins.
The continuity of editor focus is maintained only when the global focus tracker is aware of all focusable blocks of the UI. Their location in DOM is irrelevant: next to the editable root, in the {@link module:ui/editorui/bodycollection~BodyCollection "body" collection} or even somewhere else. To the global focus tracker they are all pieces of the same editor.
Check out the [Focus state analysis section ](#focus-state-analysis) to see the global focus tracker working in a real–life scenario.
Take a look at the following example of a list that has multiple items, a classic use case for a focus tracker:
import View from '@ckeditor/ckeditor5-ui/src/view';
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
class MyListView extends View {
constructor( locale ) {
super( locale );
// ...
// A view collection containing items of the list.
this.items = this.createCollection();
// ...
// The instance of the focus tracker that tracks focus in #items.
this.focusTracker = new FocusTracker();
}
// ...
}
To make sure your focus tracker instance and the items view collection stay synchronized, create listeners that will update the tracker when a new child view is added or some are removed ({@link module:ui/viewcollection~ViewCollection view collections fire events}). The best way to do that is inside the {@link module:ui/view~View#render render()} method:
// ...
class MyListView extends View {
constructor( locale ) {
// ...
}
// ...
render() {
super.render();
// Children added before rendering should be known to the #focusTracker.
for ( const item of this.items ) {
this.focusTracker.add( item.element );
}
// Make sure items added to the collection are recognized by the #focusTracker.
this.items.on( 'add', ( evt, item ) => {
this.focusTracker.add( item.element );
} );
// Make sure items removed from the collection are ignored by the #focusTracker.
this.items.on( 'remove', ( evt, item ) => {
this.focusTracker.remove( item.element );
} );
}
// ...
}
The MyListView can now track focused children, and it is time to help the user navigate them using the keyboard. In the next section, you will create a keystroke handler that will bring you closer to the goal.
KeystrokeHandler classThe {@link module:utils/keystrokehandler~KeystrokeHandler} helper class allows registering callbacks for the keystrokes. It is used in many views across the UI of the editor for many purposes. For instance, it is responsible for focusing the toolbar on the Alt+F10 keypress or it opens the link popup form when you hit Ctrl+L on a selected text.
But in the context of focus management, it is used by the focus cycler you will get familiar with in the next section. You can learn more about the {@link module:utils/keystrokehandler~KeystrokeHandler} class in the API documentation but for now, you should only know how to create and initialize it before moving forward:
import View from '@ckeditor/ckeditor5-ui/src/view';
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
export default class MyListView extends View {
constructor( locale ) {
super( locale );
// ...
// The keystroke handler that will help the focus cycler respond to the keystrokes.
this.keystrokes = new KeystrokeHandler();
}
render() {
super.render();
// ...
// Start listening for the keystrokes coming from #element, which will allow
// the focus cycler to handle the keyboard navigation.
this.keystrokes.listenTo( this.element );
}
destroy() {
// Stop listening to all keystrokes when the view is destroyed.
this.keystrokes.destroy();
}
// ...
}
FocusCycler class{@link module:ui/focuscycler~FocusCycler} helps the user navigate the user interface using the keystrokes (arrow keys, Tab, Return, Esc, etc.). This helper class was created with components hosting multiple children in mind, for instance, lists, toolbars or forms. It cycles over their children so, for instance, if the last child is focused and the user wants to move forward, the focus is moved back to the first child. It also supports components that have a variable (dynamic) number of focusable children.
Each focus cycler instance works together with a focus tracker and a keystroke handler. The former delivers the current state of the focus, while the latter helps the cycler integrate with keystrokes, for instance, to focus the next item in the list on the down arrow key press.
Take a look at the example list class using focus cycler, keystroke handler and focus tracker instances together to enable the keyboard navigation. First, all the helpers must be created:
import View from '@ckeditor/ckeditor5-ui/src/view';
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
class MyListView extends View {
constructor( locale ) {
super( locale );
// ...
// The view collection containing items of the list.
this.items = this.createCollection();
// The instance of the focus tracker that tracks focus in #items.
this.focusTracker = new FocusTracker();
// The keystroke handler that will help the focus cycler respond to the keystrokes.
this.keystrokes = new KeystrokeHandler();
// The focus cycler that glues it all together.
this.focusCycler = new FocusCycler( {
focusables: this.items,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
// Navigate list items backward using the arrow up key.
focusPrevious: 'arrowup',
// Navigate toolbar items forward using the arrow down key.
focusNext: 'arrowdown'
}
} );
}
// ...
}
Similarly to the previous sections of this guide, feed the focus tracker and synchronize it with the list items collection in the render() method. Since the {@link module:ui/view~View#element MyListView#element} has already been rendered at that stage, this is also the right moment to start listening to the keyboard events:
// ...
class MyListView extends View {
constructor( locale ) {
// ...
}
render() {
super.render();
// ...
// Items added before rendering should be known to the #focusTracker.
for ( const item of this.items ) {
this.focusTracker.add( item.element );
}
// Make sure items added to the collection are recognized by the #focusTracker.
this.items.on( 'add', ( evt, item ) => {
this.focusTracker.add( item.element );
} );
// Make sure items removed from the collection are ignored by the #focusTracker.
this.items.on( 'remove', ( evt, item ) => {
this.focusTracker.remove( item.element );
} );
// Start listening for the keystrokes coming from #element, which will allow
// the #focusCycler to handle the keyboard navigation.
this.keystrokes.listenTo( this.element );
}
focus() {
if ( this.items.length ) {
// This will call MyListItemView#focus().
this.items.first.focus();
}
}
destroy() {
// Stop listening to all keystrokes when the view is destroyed.
this.keystrokes.destroy();
}
// ...
}
The complete code of a list class that hosts multiple item views and supports the keyboard navigation across them (when it gets focused) looks as follows:
import View from '@ckeditor/ckeditor5-ui/src/view';
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
class MyListView extends View {
constructor( locale ) {
super( locale );
// The view collection containing items of the list.
this.items = this.createCollection();
// The instance of the focus tracker that tracks focus in #items.
this.focusTracker = new FocusTracker();
// The keystroke handler that will help the focus cycler respond to the keystrokes.
this.keystrokes = new KeystrokeHandler();
// The focus cycler that glues it all together.
this.focusCycler = new FocusCycler( {
focusables: this.items,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
// Navigate list items backward using the arrow up key.
focusPrevious: 'arrowup',
// Navigate toolbar items forward using the arrow down key.
focusNext: 'arrowdown'
}
} );
// ...
this.setTemplate( {
tag: 'ul',
children: this.items
} );
}
render() {
super.render();
// ...
// Items added before rendering should be known to the #focusTracker.
for ( const item of this.items ) {
this.focusTracker.add( item.element );
}
// Make sure items added to the collection are recognized by the #focusTracker.
this.items.on( 'add', ( evt, item ) => {
this.focusTracker.add( item.element );
} );
// Make sure items removed from the collection are ignored by the #focusTracker.
this.items.on( 'remove', ( evt, item ) => {
this.focusTracker.remove( item.element );
} );
// Start listening for the keystrokes coming from #element, which will allow
// the #focusCycler to handle the keyboard navigation.
this.keystrokes.listenTo( this.element );
}
focus() {
if ( this.items.length ) {
// This will call MyListItemView#focus().
this.items.first.focus();
}
}
destroy() {
// Stop listening to all keystrokes when the view is destroyed.
this.keystrokes.destroy();
}
}
class MyListItemView extends View {
constructor( locale, text ) {
super( locale );
// ...
this.setTemplate( {
tag: 'li',
attributes: {
tabindex: -1
},
children: [ text ]
} );
}
// ...
focus() {
this.element.focus();
}
}
You can quickly run it in the context of an existing editor in the following way:
ClassicEditor
.create( document.querySelector( '#editor' ), {
// ...
} )
.then( editor => {
const list = new MyListView( editor.locale );
// Create two example children.
const firstChild = new MyListItemView( editor.locale, 'First child' );
const secondChild = new MyListItemView( editor.locale, 'Second child' );
// Add children to the list.
list.items.add( firstChild );
list.items.add( secondChild );
// Render the list and put it in the DOM.
list.render();
editor.ui.view.body.add( list );
// Focus the list. This should focus the first child.
// Use up and down keyboard arrows to cycle across the list items.
list.focus();
} )
.catch( err => {
console.error( err.stack );
} );
Now you can use the keyboard arrows to cycle the focused list items:
{@img assets/img/framework-deep-dive-focus-focus-cycling.gif 950 The animation showing the focus cycling across the list items.}
This section contains an analysis of a common focus navigation scenario in an {@link examples/builds/inline-editor inline editor}. While the previous section was focused on tools that make up the focus management system, this time we will focus on their state. This should help you better understand how all little pieces work in a bigger picture.
Take a look at the following scenario where both mouse and keyboard are used to navigate the interface of the editor:
{@img assets/img/framework-deep-dive-focus-inline-scenario.gif 758 The animation showing the focus navigation across the inline editor UI.}
And here are the steps of the scenario:
There are 3 focus tracker instances at play in the scenario:
EditorUI#focusTracker} (the "global" focus tracker),LinkActionsView#focusTracker},LinkFormView#focusTracker}.Let's see how they react to the user actions (states were recorded after each step):
| Step | {@link module:core/editor/editorui~EditorUI#focusTracker `EditorUI#focusTracker`} | {@link module:link/ui/linkactionsview~LinkActionsView#focusTracker `LinkActionsView#focusTracker`} | {@link module:link/ui/linkformview~LinkFormView#focusTracker `LinkFormView#focusTracker`} | |||
|---|---|---|---|---|---|---|
isFocused |
focusedElement |
isFocused |
focusedElement |
isFocused |
focusedElement |
|
| 1 | ❌ | null |
❌ | null |
❌ | null |
| 2 | ✅ | editable | ❌ | null |
❌ | null |
| 3 | ✅ | balloon panel |
✅ ✅ |
URL preview | ❌ | null |
| 4 | ✅ | balloon panel | "Edit link" button | ❌ | null |
|
| 5 | ✅ | balloon panel | ❌ | null |
✅ | URL input |
| 6 | ✅ | balloon panel | ❌ | null |
✅ | "Save" button |
| 7 | ✅ | balloon panel | ❌ | null |
✅ | "Cancel" button |
| 8 | ✅ | editable | ❌ | null |
❌ | null |
| 9 | ✅ | editable | ❌ | null |
❌ | null |
editor.ui.focusTracker) is always aware of the focus state, even when the focus is in the farthest regions of the UI.
LinkActionsView and LinkFormView only know about the focus as long as one of their children has it.LinkActionsView and LinkFormView know precisely which element has focus. This is their region of interest and, unlike the global focus tracker of the editor, they need that information to allow navigation using the keyboard.