Contribute to this guide

Deep dive into 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.

# What is focus and why it matters for CKEditor?

# What is focus?

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.

The animation shows 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 without this 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.

# Focus in CKEditor

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.

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:

  • The first section explains how the editor engine manages focus and what tools exist there to help you out when developing new features.
  • In the second section, we will show you how the user interface of the editor tracks focus and how various UI components take advantage of that, for instance, to provide accessibility.
  • In the last part, we will use the knowledge from previous sections in a real–life scenario and analyze how all these tools and systems work together.

# Focus in the editor engine

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 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 FocusObserver (learn more about view observers) to track focus in editable elements by listening to native DOM focus and blur events coming from them.

Already confused? Take a look at the editing engine guide that explains what the editing view, editable elements, and other building blocks of CKEditor 5 are.

# Checking if the view document is focused

You can check if the view document is focused using its observable 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 (for example, 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.

How do you 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 multiple editing roots and navigate across them.

To spice things up even more, you should also know isFocused will change when you focus any nested editable in the content (take, for example, a 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.

# How to focus the editor?

The simplest way to focus the editor is to call the editor.focus() method.

However, you may wish to explicitly focus the editable area of CKEditor 5 when a certain action is executed (for example, a button is clicked). To do that, use the 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 first editable. If an editor has many 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 setSelection() method of the model writer to change the selection.

# Focus in the editor UI

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 editor engine. But because the 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 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 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:

  • To make sure the editor (as a whole) never loses focus unless the user wants it to.
    For instance, take a look at the inline editor. As long as the user edits text in the main editable area or configures its properties in any pop-up, dropdown, or panel, the main editor toolbar must remain visible (because the focus is somewhere in the UI). Only when the user finishes editing and moves somewhere else on a web page, the toolbar can disappear.
  • To make the UI accessible to users who navigate it using screen readers and other assistive technologies.
    These users not only write text using the keyboard but also use it to navigate across toolbar buttons, panels, dropdowns, etc. The UI of the editor must constantly keep track of which component is currently focused, for example, to allow navigation using Tab, Esc and arrow keys.

# Tools and architecture

Focus management lives next to the 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 classic editor instance:

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:

The image showing the focus layers used during navigation.

  1. The root of the focus tree is the ClassicEditorUI class. It creates a global focus tracker for the entire editor (you can access it via editor.ui.focusTracker).
  2. When editing text, you can hit the Alt+F10 keystroke to focus the main editor toolbar, which is the second focus layer.
    • The ToolbarView component brings a focus tracker that keeps an eye on its children so that when a user navigates across the toolbar using the keyboard arrows, it is clear which item is focused.
    • Toolbars also use a focus cycler to provide continuous navigation. For instance, navigating to the next item when the last one is focused brings the focus back to the beginning of the toolbar.
  3. When a user selects a toolbar item, its focus() method is executed. That brings us to the third layer.
    • Some components of the toolbar are simple (like ButtonView) and their DOM elements are leaves of the focus tree. But some components, for instance, the DropdownView, are containers and allow the focus to go deeper. Each dropdown has a focusable button and can host one child view in its DropdownPanelView. It does not need a focus tracker because there are only two ways a user can navigate it: down its panel or away from it.
  4. When a user enters the dropdown’s panel, they visit another (fourth) layer of focus. This time it is the ListView that, like the ToolbarView, also needs a focus tracker and a focus cycler to allow smooth keyboard navigation across its items.
  5. The outermost leaf of the focus tree in this example is a ListItemView. It has the 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 ClassicEditorUI
2 ToolbarView
3 DropdownView
4 ListView
5 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:

# Implementing focusable UI components

Any UI view can be focusable. To become one, a view must implement the focus() method that focuses the DOM 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" 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 'ckeditor5';

class MyListItemView extends View {
    constructor( locale, text ) {
        super( locale );

        // More initializations.
        // ...

        this.setTemplate( {
            tag: 'li',
            attributes: {
                tabindex: -1
            },
            children: [ text ]
        } );
    }

    // More methods.
    // ...

    focus() {
        this.element.focus();
    }
}

If a view has many focusable children (like a list), the focus() method should focus the first child:

import { View } from 'ckeditor5';

class MyListView extends View {
    constructor( locale ) {
        super( locale );

        // More initializations.
        // ...

        this.items = this.createCollection();

        this.setTemplate( {
            tag: 'ul',
            children: this.items
        } );
    }

    // More methods.
    // ...

    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.

# Using the FocusTracker class

One of the key focus management helpers in the CKEditor 5 UI is the FocusTracker class and its instances. They work at the DOM level and offer a fairly simple API:

  • Methods to add() and remove() tracked DOM elements.
  • An observable isFocused property telling the world that one of the tracked elements has focus in the DOM.
  • An observable 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 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.

# A note about the global focus tracker

Each editor instance has a global focus tracker that can be accessed via 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 (ClassicEditorUI, 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 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 “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 to see the global focus tracker working in a real–life scenario.

# A practical example

Take a look at the following example of a list that has multiple items, a classic use case for a focus tracker:

import { View, FocusTracker } from 'ckeditor5';

class MyListView extends View {
    constructor( locale ) {
        super( locale );

        // More initializations.
        // ...

        // A view collection containing items of the list.
        this.items = this.createCollection();

        // Setting the template.
        // ...

        // The instance of the focus tracker that tracks focus in #items.
        this.focusTracker = new FocusTracker();
    }

    // More methods.
    // ...
}

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 (view collections fire events). The best way to do that is inside the render() method:

// Previously imported packages.
// ...

class MyListView extends View {
    constructor( locale ) {
        // More initializations.
        // ...
    }

    // More methods.
    // ...

    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 );
        } );
    }

    // More methods.
    // ...
}

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.

# Using the KeystrokeHandler class

The 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 key press or it opens the link pop-up form when you hit Ctrl+L on a selected text.

However, 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 KeystrokeHandler class in the API documentation but for now, you should only know how to create and initialize it before moving forward:

import { FocusCycler, View, FocusTracker, KeystrokeHandler } from 'ckeditor5';

export default class MyListView extends View {
    constructor( locale ) {
        super( locale );

        // More initializations.
        // ...

        // The keystroke handler that will help the focus cycler respond to the keystrokes.
        this.keystrokes = new KeystrokeHandler();
    }

    render() {
        super.render();

        // Setting the focus tracker.
        // ...

        // 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();
    }

    // More methods.
    // ...
}

# Using the FocusCycler class

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 { FocusCycler, View, FocusTracker, KeystrokeHandler } from 'ckeditor5';

class MyListView extends View {
    constructor( locale ) {
        super( locale );

        // More initializations.
        // ...

        // 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'
            }
        } );
    }

    // More methods.
    // ...
}

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 MyListView#element has already been rendered at that stage, this is also the right moment to start listening to the keyboard events:

// Previously imported packages.
// ...

class MyListView extends View {
    constructor( locale ) {
        // More initializations.
        // ...
    }

    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();
    }

    // More methods.
    // ...
}

# Using all focus helpers together

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 { FocusCycler, View, FocusTracker, KeystrokeHandler } from 'ckeditor5';

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'
            }
        } );

        // More intializations.
        // ...

        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 );

        // More initializations.
        // ...

        this.setTemplate( {
            tag: 'li',
            attributes: {
                tabindex: -1
            },
            children: [ text ]
        } );
    }

    // More methods.
    // ...

    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' ), {
        // The editor's configuration.
        // ...
    } )
    .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:

The animation showing the focus cycling across the list items.

# Focus state analysis

This section contains an analysis of a common focus navigation scenario in an 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.

# Scenario

Take a look at the following scenario where both mouse and keyboard are used to navigate the interface of the editor:

The animation showing the focus navigation across the inline editor UI.

And here are the steps of the scenario:

  1. The editor is not focused (the focus is somewhere else on the web page).
  2. The editable area gets focused using the mouse. The main toolbar shows up and because the link was clicked, the link actions view also pops up.
  3. The Tab key is used to focus the link preview in the balloon (a child of LinkActionsView).
  4. The Tab key is used to focus the “Edit link” button.
  5. The Space key is used to execute the “Edit link” button. The focus moves to the input in the LinkFormView.
  6. The Tab key is used to move from the link URL field to the “Save” button.
  7. The Tab key is used to move from the “Save” button to the “Cancel” button.
  8. The Space key is used to execute the “Cancel” button and close the editing form.
  9. The Esc key is used to close the link balloon and go back to the editable.

There are 3 focus tracker instances at play in the scenario:

  1. The EditorUI#focusTracker (the “global” focus tracker),
  2. The LinkActionsView#focusTracker,
  3. The LinkFormView#focusTracker.

Let’s see how they react to the user actions (states were recorded after each step):

Step EditorUI#focusTracker LinkActionsView#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

# Conclusions

  • The global focus tracker (the one you can access via editor.ui.focusTracker) is always aware of the focus state, even when the focus is in the farthest regions of the UI.
    • It does not know which element is focused on deeper layers (for instance the “Edit link” button), though. All it knows is where the focus went (for example, from the editable to the balloon panel).
    • It lacks precise information about the focus in the link UI because this is the responsibility of the focus tracker of the link UI layer.
    • All editor features can always depend on the global focus tracker when necessary. For instance, the main editor toolbar is displayed as long as the global focus tracker knows the focus is somewhere in the editor.
  • You can see that the focus management is modular: LinkActionsView and LinkFormView only know about the focus as long as one of their children has it.
  • Focus trackers belonging to 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.