completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Keyboard & focus

Handle keyboard events, focus movement, focus traps, disclosures, and live regions without breaking accessibility.

By the end you can
  • 01
    Read key eventsUse keydown and keyup, tell key from code, and check modifier keys, repeats, and IME typing.
  • 02
    Move focusExplain the Tab order, tabindex, which focus events bubble, document.activeElement, and focus({ preventScroll: true }).
  • 03
    Build controls everyone can useKeep focus inside a dialog, send it back to the opener, and tell screen readers about changes with aria-expanded and live regions.

Using a page with only the keyboard

Many people use a page with only the keyboard. Some can't use a mouse. Others just find the keyboard faster. A keyboard user can't click on things. Instead, they press Tab to move from one control to the next. A control is anything you can use on a page, like a button, a link, or a text box.

The control that is ready for the keyboard right now has focus. Only one element has focus at a time. When you press a key, the browser sends a key event to that element. So this lesson is about two things that work together:

  • Key events tell you which key was pressed.
  • Focus tells you which element gets those key events.
You press Enter. A page has three controls: search, Save, and Help. Only Save has focus, so the Enter key event goes to Save.you pressEnterpagesearchSaveHelpfocusedgets the key
Only one element has focus at a time. When you press a key, the key event goes to that element.

If people can't reach a control, open it, close it, and understand it with only the keyboard, they can't use it. In this lesson you'll try a key inspector, a shortcut, a focus log, a dialog that keeps focus inside, and a message that is read aloud.

Real-life analogyFocus is a stage spotlight

Think of a theater stage with one spotlight. The audience listens to the actor in the light. The spotlight moves from actor to actor. Sometimes a small scene plays in one corner, and the light stays in that corner until the scene ends.

In real life: One actor stands in the spotlight
In JavaScript: One element has focus. JavaScript calls it document.activeElement
In real life: The spotlight moves to the next actor
In JavaScript: Tab moves focus to the next control
In real life: A small scene keeps the spotlight in one corner
In JavaScript: An open dialog keeps focus inside it
In real life: The spotlight goes back to the actor who started the scene
In JavaScript: When the dialog closes, focus goes back to the button that opened it
The main idea

Keyboard and focus code reacts to keys on the element that has focus. It must also keep these things working: typing text, moving with Tab, closing with Escape, and the messages that screen readers read aloud. A screen reader is a program that reads the page out loud, for people who can't see the screen.

keydown and keyup

INTERACTIVE

When you press a key, the browser fires two events:

  • keydown fires when the key goes down.
  • keyup fires when the key comes back up.

If you hold a key down, most browsers fire keydown again and again. Each extra event is a repeat. On a repeat, event.repeat is true. The keyup event fires only once, when you let go.

There is also an old event called keypress. It is deprecated: browsers keep it only for old code, and new code should not use it. To know when the text in a field changes, listen for the input event instead. It fires for every way text can change:

  • typing on a keyboard
  • pasting
  • autocomplete, when the browser fills in a saved value
  • phone keyboards and speech
  • an input method editor (IME): a tool that builds one character from several key presses, used for languages like Chinese and Japanese
Read the key name and the repeat flagPop out in the code editor (opens in a new tab)JavaScript
const event = { key: "Enter", repeat: false };
console.log(event.key);
console.log(event.repeat);

This code uses a plain object that looks like a key event, so it can run anywhere. Line 1 makes the object. Line 2 prints the name of the key, Enter. Line 3 prints false, because this is the first event, not a repeat.

Text changes as a whole valuePop out in the code editor (opens in a new tab)JavaScript
const field = { value: "Asha" };
field.value = "Asha Patel";
console.log(field.value);

Line 1 makes an object that stands for a text field holding Asha. Line 2 changes the whole value at once. Line 3 prints Asha Patel. A real input event tells you the new value in the same way. It doesn't matter if the text came from one key, a paste, autocomplete, speech, or an IME.

Key inspector: press real keys
Listen for keydown and keyupPop out in the code editor (opens in a new tab)JavaScript
box.addEventListener("keydown", (event) => {  if (event.key === " " || event.key === "ArrowDown") event.preventDefault();  show({    key: event.key,    code: event.code,    repeat: event.repeat,    shiftKey: event.shiftKey,    ctrlKey: event.ctrlKey,    altKey: event.altKey,    metaKey: event.metaKey,    capsLock: event.getModifierState("CapsLock"),    isComposing: event.isComposing,  });}); box.addEventListener("keyup", showRelease);// `keypress` is deprecated; use `input` for text and `keydown`/`keyup` for keys.
What the browser reports0 recent events
Click here firstTry A, Shift+A, Space, the arrow keys, Caps Lock, and holding a key down.
    Try it yourself

    Click the box, then press keys.

    The box has tabindex="0", so it can get focus. Inside the box, Space and ArrowDown don't scroll the page. What IME typing shows depends on your keyboard settings.

    Look at the modifier flags in the log. A modifier is a key you hold while you press another key:

    • shiftKey is true while Shift is held.
    • ctrlKey is true while Ctrl is held.
    • altKey is true while Alt is held. On a Mac, this key is called Option.
    • metaKey is true while Command is held on a Mac.

    Caps Lock works differently, because it stays on after you let go. To check it, call event.getModifierState("CapsLock"). While someone is building a character with an IME, event.isComposing is true. Shortcut code should ignore those key presses, because the person is still typing. A command that should run only once should also ignore events where event.repeat is true.

    key and code: what it types, or where it sits

    STEP THROUGH

    Every key event has two names for the key:

    • event.key is what the key types right now, with the person's keyboard layout. Examples: "a", "A", "Enter", "ArrowLeft", and " " (one space) for the Space bar.
    • event.code is where the key sits on the keyboard, no matter what it types. Examples: "KeyA", "Digit1", "Space", and "ShiftLeft".

    A keyboard layout is which letter goes on which key. It is different in different countries. On a French keyboard, the key where a US keyboard has Q types the letter a. So for that key, event.key is "a", but event.code is still "KeyQ".

    One key press on a French keyboard, on the key where a US keyboard has Q. Its key is a, what it types. Its code is KeyQ, where it sits.one key pressAFrench keyboardkey: "a"what it typescode: "KeyQ"where it sits
    On a French keyboard, the key in the US Q spot types a. event.key says what it types; event.code says where the key sits.

    Some layouts have dead keys. A dead key adds an accent to the next letter: press the accent key, then e, and you get é. When you press the dead key itself, event.key is "Dead", because nothing is typed yet.

    Real-life analogyThe letter on a key, and the key's spot

    Look at a keyboard. Each key has a letter printed on it, and each key sits in a spot. In another country, the same spot can have a different letter printed on it. key is the printed letter. code is the spot.

    In real life: The letter printed on the key
    In JavaScript: event.key: what the person meant to type
    In real life: The key's spot on the keyboard
    In JavaScript: event.code: where the key sits
    In real life: A French keyboard prints a different letter in the same spot
    In JavaScript: Same spot: code is KeyQ, but key is a
    In real life: A game cares about the spot, not the letter
    In JavaScript: Games that move with W, A, S, and D often use code
    Step through a Ctrl+K or Command+K shortcut
    Step 0 of 6Ready
    Your turn: follow the blue line

    Step through a shortcut check that leaves text fields alone.

    Running in
    1. script
    Next: line 1
    const event = { key: "k", metaKey: true, ctrlKey: false, repeat: false, isComposing: false };const editingText = target === "input";const commandKey = event.metaKey || event.ctrlKey;const opensPalette = commandKey && event.key.toLowerCase() === "k" && !editingText && !event.repeat && !event.isComposing;console.log(opensPalette ? "open palette" : "let the browser type");
    CallStoreChangeResultBlue line: runs next. Ran: already executed.
    Recent returnsNothing yet. Start with the blue line.
    Where is focus when the shortcut is pressed?

    Pick one, then step through again. On the page, the palette opens. In a text field, the keys are left alone for typing.

    A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

    A shortcut is a key combination that runs a command, like Ctrl+K to open a search box. A good shortcut listens only inside the part of the page that owns it. The demo below opens a command palette: a small box where you type a command. It follows three rules:

    • It listens only inside its own demo area.
    • It accepts Command+K on a Mac and Ctrl+K everywhere else.
    • It calls preventDefault() only for that exact shortcut. preventDefault() stops the browser's own action for the key, so every other key keeps working as normal.
    Shortcut demo: open a small command palette
    A shortcut listener for one areaPop out in the code editor (opens in a new tab)JavaScript
    demo.addEventListener("keydown", (event) => {  const editing = event.target.matches("input, textarea, [contenteditable]");  const command = event.metaKey || event.ctrlKey;  if (command && event.key.toLowerCase() === "k" && !editing && !event.repeat && !event.isComposing) {    event.preventDefault();    openPalette();  }});
    Demo area (click to focus)palette closed
      Try it yourself

      Click the demo area and press Ctrl+K or Command+K. Then try the same keys in the text field: there, the keys are left for typing.

      The listener is only on this demo area, not on the whole page. It does nothing while a text field has focus.
      key, code, or input?
      • An undo shortcut, Ctrl+Z
      • Search when someone presses Enter
      • A game that moves with W, A, S, and D
      • A web piano played on the keys from KeyA to KeyL
      • Someone types their name into a text field
      • Count the letters in a comment box
      Try it yourself
      0 of 6 correct

      For each case, pick what your code should check first.

      Choose a category for every card. You can change an answer at any time; Reset clears them all.

      Which elements get focus, and the four focus events

      INTERACTIVE

      Focus is more than the ring you see around a control. It decides where key presses go. Some elements can get focus on their own: links, buttons, text fields, select menus, and text areas. Any other element needs the tabindex attribute:

      • tabindex="0" puts the element in the normal Tab order, in the order it appears on the page.
      • tabindex="-1" lets your code focus the element, but Tab skips it.
      • A positive number, like tabindex="3", makes Tab visit the element before the normal order. Avoid it, because the order becomes hard to predict.

      When focus moves, the browser fires four events: focus, blur, focusin, and focusout. Only two of them bubble. Bubbling means the event also reaches the parent elements, so one listener on a container hears every child.

      Left: focus on an input does not bubble, so a normal listener on the container misses it. Right: focusin on the input bubbles up, so the container hears it.focuscontainerdoes not bubbleinputcontainer misses itfocusincontainerbubbles upinputcontainer hears it
      Both events fire when the input gets focus. Only focusin bubbles up, so only it reaches a normal listener on the container.

      Each row is one focus event. Read across to see if it bubbles, and what it is good for.

      The four focus events
      EventDoes it bubble?What it is good for
      focusNo.React when one element gets focus. A container hears it only with a capture listener.
      blurNo.React when one element loses focus. A container hears it only with a capture listener.
      focusinYes.One listener on a container hears focus arrive on any child.
      focusoutYes.One listener on a container hears focus leave any child.
      Focus event log
      Listen for all four focus eventsPop out in the code editor (opens in a new tab)JavaScript
      container.addEventListener("focus", log, true);container.addEventListener("blur", log, true);container.addEventListener("focusin", log);container.addEventListener("focusout", log); laterButton.focus({ preventScroll: true });console.log(document.activeElement);
      Controls that can get focusactive: (none yet)
      Custom panel with tabindex 0
        Try it yourself

        document.activeElement is (none yet). Press Tab to move through the controls. Compare focus and blur, which don't bubble, with focusin and focusout, which do.

        relatedTarget is null when focus comes from, or goes to, somewhere outside this page.

        document.activeElement is the element that has focus right now. Check it after the focus move is done. Each focus event also has a relatedTarget: the other element in the move.

        • For focus and focusin, it is the element that just lost focus.
        • For blur and focusout, it is the element that gets focus next.
        • It is null when focus comes from, or goes to, somewhere outside the page.

        To move focus with code, call element.focus(). If the jump would scroll the page and confuse people, write element.focus({ preventScroll: true }) instead.

        Two tabindex values: Tab order, or code onlyPop out in the code editor (opens in a new tab)JavaScript
        const tabOrder = { customBox: 0, dialogTitle: -1 };
        console.log(tabOrder.customBox);
        console.log(tabOrder.dialogTitle);

        This is a plain object, not the real page. Line 2 prints 0: a custom box that belongs in the normal Tab order. Line 3 prints -1: a dialog title that code can focus, but Tab skips.

        Make focus easy to see. The CSS selector :focus-visible matches a focused element when the browser thinks a ring helps, mostly when someone uses the keyboard. So you can show a strong ring for keyboard users without showing it on every mouse click.

        Keeping focus inside a dialog

        INTERACTIVE

        A modal dialog is a box that opens on top of the page. The page behind it can't be used until the dialog closes. While the dialog is open, three things should be true:

        • Tab and Shift+Tab move only between the controls inside the dialog. This is called a focus trap.
        • Escape closes the dialog, unless there is a good reason not to.
        • When the dialog closes, focus goes back to the button that opened it.

        Without these, a keyboard user can Tab to controls hidden behind the dialog, or lose their place on the page.

        A dialog with three controls in a row: input, Save draft, and Close. Tab moves from one to the next. Tab on Close, the last control, wraps back to input, the first.dialoginputTabSave draftTabCloseTab wraps to the first
        Tab moves through the dialog's controls. On the last one, the trap sends focus back to the first, so focus never leaves the dialog. Shift+Tab does the same in the other direction.
        Move through a list and wrap aroundPop out in the code editor (opens in a new tab)JavaScript
        const items = ["Home", "Settings", "Help"];
        let index = 0;
        function move(direction) {
          index = (index + direction + items.length) % items.length;
          return items[index];
        }
        console.log(move(1));
        console.log(move(-1));

        The same wrap-around idea works for any list, so start with a simple one. Line 1 makes a list of three names. Line 2 starts at index 0, Home. On line 4, move adds the direction (1 or -1) to the index. The % sign gives the remainder after dividing by the list length. That makes the index wrap: going past the end lands on the start. Adding items.length first keeps the number from going below 0, so going before the start lands on the end.

        Line 7 moves down and prints Settings. Line 8 moves back up and prints Home. A real list or menu also needs a clear style for the current item and a name for screen readers. This code only moves the index.

        Focus trap: keep focus inside the dialog
        The focus trap you write yourselfPop out in the code editor (opens in a new tab)JavaScript
        const focusables = [...dialog.querySelectorAll("button, input, [href]")];dialog.addEventListener("keydown", (event) => {  if (event.key === "Escape") closeDialog();  if (event.key !== "Tab") return;  const first = focusables[0];  const last = focusables.at(-1);  if (event.shiftKey && document.activeElement === first) {    event.preventDefault();    last.focus();  } else if (!event.shiftKey && document.activeElement === last) {    event.preventDefault();    first.focus();  }});
        Dialog areaclosed
        Try it yourself

        Open the dialog. The built-in <dialog> with showModal() does some of this work for you. This demo shows the trap you write yourself when you build your own dialog.

        Use the built-in <dialog> when it fits. A dialog you build yourself also needs an inert page behind it, a name, Escape to close, and focus sent back.

        There are two ways to build a modal dialog. Each row shows what you get, and what you still have to do yourself.

        Built-in dialog or a trap you write yourself
        Which wayWhat you getWhat you still do yourself
        dialog.showModal()The page behind becomes inert: it can't be clicked or focused. Escape closes the dialog.Give the dialog a name, choose where focus starts, and send focus back to the opener, because browsers differ here.
        A trap you writeIt works for a custom box when the built-in dialog doesn't fit.Wrap Tab and Shift+Tab, handle Escape, make the page behind inert, name the dialog, and send focus back.
        The same wrap, for focus in a dialogPop out in the code editor (opens in a new tab)JavaScript
        const focusables = ["open", "cancel", "save"];let activeIndex = 0;function moveFocus(direction) {  activeIndex = (activeIndex + direction + focusables.length) % focusables.length;  return focusables[activeIndex];}console.log(moveFocus(1));console.log(moveFocus(-1));

        This list holds three controls, and focus starts on open. Line 7 moves forward and prints cancel. Line 8 moves back and prints open. A real trap uses the same wrap for Tab and Shift+Tab.

        Telling screen readers what changed

        INTERACTIVE

        ARIA attributes are extra attributes that describe the page to screen readers. They only describe things. They don't add any behavior.

        A disclosure is a button that shows or hides a panel, like a Filters button. Give the button two attributes:

        • aria-expanded says if the panel is open: "true" or "false".
        • aria-controls holds the panel's id, so screen readers know which panel the button opens.

        The button still needs real button behavior. ARIA doesn't add a click handler or a key handler for you.

        A live region is an element that a screen reader reads aloud whenever its text changes. Focus doesn't move to it. You make one with the aria-live attribute, or with a role such as role="status".

        Real-life analogyaria-live is a stadium announcer

        At a stadium, an announcer reads out the score. The fans keep watching the game while they listen. Normal news waits for a quiet moment. Only an emergency message cuts in at once.

        In real life: The announcer reads out the new score
        In JavaScript: A live region reads out its new text
        In real life: The fans keep watching the game
        In JavaScript: Focus stays where it was
        In real life: Normal news waits for a quiet moment
        In JavaScript: aria-live="polite" or role="status"
        In real life: An emergency message cuts in at once
        In JavaScript: aria-live="assertive" or role="alert", only for urgent messages
        Change the expanded statePop out in the code editor (opens in a new tab)JavaScript
        const button = { "aria-expanded": "false" };
        button["aria-expanded"] = "true";
        console.log(button["aria-expanded"]);

        Line 1 makes an object that stands for the button, with aria-expanded set to "false". Line 2 changes it to "true", and line 3 prints true. On a real page, your click handler must also show or hide the panel. The attribute only describes it.

        A Filters button and a live status message
        Set the open state and the status textPop out in the code editor (opens in a new tab)JavaScript
        button.setAttribute("aria-expanded", String(open));button.setAttribute("aria-controls", "filter-panel");panel.hidden = !open; status.textContent = `3 results found`;// The live region existed before this text changed.
        Filters3 results found
        • Buttons
        • Dialogs
        • Live regions
        Try it yourself

        The button has aria-expanded=false. The live status message was on the page from the start, and now it says: 3 results found.

        role="status" waits for a quiet moment. Use role="alert" or aria-live="assertive" only for urgent messages.

        Put the live region on the page first, and change its text later. If the region and its text appear at the same moment, screen readers may miss the text. Use polite for normal updates, like "3 results found". Use assertive or role="alert" only for urgent messages, because they interrupt whatever the screen reader is saying.

        Where you'll use keyboard and focus code

        Keyboard and focus code shows up in most real websites:

        • search boxes that search when you press Enter
        • menus that close when you press Escape
        • command palettes that open with Ctrl+K or Command+K
        • skip links: a link at the top that jumps past the menu to the main content
        • a list of form errors that gets focus after a failed submit
        • panels that open and close, and lists that suggest words as you type
        • modal dialogs

        Here is the shortcut check from the step-through again, as one block of code:

        A shortcut that lets people keep typingPop out in the code editor (opens in a new tab)JavaScript
        const target = "page";const event = { key: "k", metaKey: true, ctrlKey: false, repeat: false, isComposing: false };const editingText = target === "input";const commandKey = event.metaKey || event.ctrlKey;const opensPalette = commandKey && event.key.toLowerCase() === "k" && !editingText && !event.repeat && !event.isComposing;console.log(opensPalette ? "open palette" : "let the browser type");

        Line 3 checks if focus is in a text field. Line 4 accepts Command or Ctrl. Line 5 opens the palette only for K, only outside a text field, and not for a repeat or IME typing. Focus is on the page here, so line 6 prints open palette.

        Four rules to remember:

        • Use keydown for commands. Use input to check text.
        • Check where focus is before you take over a key.
        • Call preventDefault() only for the exact keys you own.
        • When a dialog or menu closes, send focus back to where it was.

        Common mistakes

        • "keypress is the event for text." keypress is deprecated. Use input to know when text changes.
        • "key and code are the same." key is what the key types with the current layout. code is where the key sits.
        • "A div with a click handler is a button." A real button already gets focus, works with Enter and Space, can be disabled, and tells screen readers it is a button.
        • "focus and blur bubble." They don't. Use a capture listener, or use focusin and focusout, which do bubble.
        • "A dialog is done once it looks right." It also has to keep focus inside, make the page behind it inert, have a name, close on Escape, and send focus back.
        • "A live region always works the same way." Screen readers differ, so test your live regions with real screen readers.

        Practice exercises

        5 EXERCISES
        Exercise 1 · Warm-upWhich event repeats?

        Answer from memory: which key event fires again and again while you hold a key down?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 2 · PracticeRead the output

          Read the code. What exact text does line 2 print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const event = { key: "Enter", repeat: false };
          console.log(event.key);
          console.log(event.repeat);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 3 · PracticeWrite a safe shortcut check

            Write a function that says when to open a command palette: on Command+K or Ctrl+K, but not while the person is typing in a text field.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            function shouldOpenPalette(event, editingText) {
              const command = event.metaKey || event.ctrlKey;
              return command && event.key.toLowerCase() === "k" && !editingText;
            }
            console.log(shouldOpenPalette({ key: "k", metaKey: true, ctrlKey: false }, false));
            console.log(shouldOpenPalette({ key: "k", metaKey: true, ctrlKey: false }, true));
              Exercise 4 · PracticeFind the focus bug

              A form section has a normal focus listener on the container. It never hears the inputs inside get focus. Which event, which does bubble, should it listen for instead?

              Answer, then press Check. Spacing and letter case don’t matter.

                Exercise 5 · ChallengePick a tabindex value

                You made a custom panel. People should reach it with Tab, in the normal page order. Which tabindex value does that?

                Answer, then press Check. Spacing and letter case don’t matter.

                  Check your understanding

                  7 QUESTIONS
                  Keyboard and focus quiz · 7 questionsScore: first tries count
                  1. Question 1 of 7In most browsers, what happens while you hold a key down?

                    Choose an answer to see the explanation.

                  2. Question 2 of 7What does this code print?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    const event = { key: "Enter", code: "NumpadEnter" };
                    console.log(event.key);

                    Choose an answer to see the explanation.

                  3. Question 3 of 7On a French keyboard, the key where a US keyboard has Q gives key: "a" and code: "KeyQ". Which property tells you what the person meant to type?

                    Choose an answer to see the explanation.

                  4. Question 4 of 7Which event should you listen for to know when the text in a field changes?

                    Choose an answer to see the explanation.

                  5. Question 5 of 7Which focus events bubble up from a child to its container?

                    Choose an answer to see the explanation.

                  6. Question 6 of 7Focus moves back from the first control. What does this code print?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    const focusables = ["cancel", "save"];
                    let index = 0;
                    index = (index - 1 + focusables.length) % focusables.length;
                    console.log(focusables[index]);

                    Choose an answer to see the explanation.

                  7. Question 7 of 7Which sentence about live regions is true?

                    Choose an answer to see the explanation.

                  Key takeaways

                  • keydown fires when a key goes down, and repeats while you hold it. Use it for commands. keyup fires once, when you let go.
                  • key is what the key types. code is where the key sits on the keyboard.
                  • Use the input event for text changes. It also covers pasting, IME typing, phone keyboards, and autocorrect.
                  • focus and blur don't bubble. focusin and focusout do.
                  • A modal dialog keeps focus inside, closes on Escape, makes the page behind it inert, and sends focus back when it closes.
                  • aria-expanded says if a panel is open. A live region reads out new text without moving focus.
                  In one sentence

                  Good keyboard code sends key commands to the element that has focus, and it keeps every control reachable and every change clear to screen readers.

                  Up next: Page & resource loading.

                  CompleteFrontend Clear concepts. Working examples.