OTTODesign System

Code

Floating Focus

Storybook group: Components · Sidebar path: Components/Floating Focus · Extracted 28.09.2026

Version Tag Status API
v1 <oc-floating-focus-v1> Stable, allowed for generation -

API from Storybook controls (FloatingFocusV1.stories.ts)

No generated API page exists for this component; these are the documented controls (argTypes).

Name Category Type Default Description
--oc-floating-focus-v1-radius CSS Variables CSS Value Border radius of the focus ring.
--oc-floating-focus-v1-offset CSS Variables CSS Value Offset of the floating focus from the element. This can be used to adjust the position of the focus ring.
--oc-floating-focus-v1-preserve-outline CSS Variables CSS Value Keeps the outline of the element visible. By default, the floating focus removes the outline. Set to true to enable outline preservation.
--oc-floating-focus-v1-full-bleed CSS Variables CSS Value Expands the floating focus to cover the entire parent element. Set to true to enable.
data-oc-floating-focus-v1-masked Data Attributes boolean Toggles masking of a child element to create a cut-out effect for the floating focus. Requires a data-oc-floating-focus-v1-mask-element attribute on the child element that should be masked.

Overview (v1)

Source: ./src/components/floating-focus/v1/Overview.mdx

Floating focus

The floating focus component is a customizable floating focus component that globally enhances the focus state of elements on a page. It provides a consistent and visually appealing way to indicate focus, improving accessibility and user experience. The floating focus component fly's around the screen, following the user's focus, and can be styled to match your design preferences.

Default variation

Story Default:

<oc-floating-focus-v1></oc-floating-focus-v1>

Configuration

The floating focus component is configurable via CSS variables on the focus target element. To explore all the available options and adjust the component, use the component configurator and see the changes affect the component in real-time.

Usage guidelines

Before integrating the Floating focus component into your project, make sure you have correctly installed the OTTO components package. Look through the variations page for examples of possible component variations. Here, you can discover both common and specific variations that address different use cases.

Configuration (v1)

Source: ./src/components/floating-focus/v1/Configuration.mdx

Floating focus configuration

Configure the focus target for the floating focus component with the controls below and see the changes live in the preview canvas. Click the Show code button within the preview canvas to see the source code for the current focus target configuration.

Story Default:

<oc-floating-focus-v1></oc-floating-focus-v1>

Interactive configurator (Storybook controls); every option is listed in the API section of this file.

Variations (v1)

Source: ./src/components/floating-focus/v1/Variations.mdx

Variations

Listed below are the most common variations of the floating focus component as well as specific component variations for different use cases.

You can explore all available options using the component configurator, adjust the component to your needs, and see the changes live in a preview canvas.

Default

Story: components-floating-focus-variations--default · tags: components

Default behavior of the Floating Focus component.

<oc-floating-focus-v1></oc-floating-focus-v1>

Border Radius

Story: components-floating-focus-variations--border-radius · tags: components

Variation to demonstrate how the border radius can be adjusted.

Args: --oc-floating-focus-v1-radius=32px

<oc-floating-focus-v1 style="--oc-floating-focus-v1-radius: 32px"></oc-floating-focus-v1>

Outline Offset

Story: components-floating-focus-variations--outline-offset · tags: components

Variation to demonstrate how the border radius can be adjusted.

Args: --oc-floating-focus-v1-offset=32px

<oc-floating-focus-v1 style="--oc-floating-focus-v1-offset: 32px"></oc-floating-focus-v1>

Preserve Outline

Story: components-floating-focus-variations--preserve-outline · tags: components

Variation with custom outline preservation for the focused element.

Args: --oc-floating-focus-v1-preserve-outline=true

<oc-floating-focus-v1 style="--oc-floating-focus-v1-preserve-outline: true"></oc-floating-focus-v1>

Full Bleed

Story: components-floating-focus-variations--full-bleed · tags: components

Variation using full bleed to cover the entire parent element.

Args: --oc-floating-focus-v1-full-bleed=true

<oc-floating-focus-v1 style="--oc-floating-focus-v1-full-bleed: true"></oc-floating-focus-v1>

Demo: Masked Element

Story: components-floating-focus-variations--demo-masked · tags: components

<oc-floating-focus-v1></oc-floating-focus-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Masked Element",
  render() {
    return `
      <div tabindex="0" data-oc-floating-focus-v1-masked style="margin: 50px; position: relative; height: 200px; width: 200px; background: lightgreen;">
        <div style="position: absolute; top: -20px; left: -20px; height: 40px; width: 40px; background: green;" data-oc-floating-focus-v1-mask-element>
          
        </div>
      </div>
    `;
  }
}

Interaction tests (FloatingFocusV1.interactions.stories.ts)

Interaction test stories (automated tests, not usage patterns).

Should Handle Element Destroy

Story: components-floating-focus-interaction-tests--should-handle-element-destroy · tags: play-fn

<div>
  <button>Focus</button>
</div>
${visibleHost}
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <div>
        <button>Focus</button>
      </div>
      ${visibleHost}
    `;
  },
  async play({
    canvasElement
  }) {
    const ocFloatingFocus = document.body.querySelector("oc-floating-focus-v1")! as HTMLElement;
    const shadowFloater = ocFloatingFocus.shadowRoot!.querySelector(".floating-focus")! as HTMLElement;
    const button = canvasElement.querySelector("button")!;
    keyboardFocus(button);
    await waitFor("should match button dimensions", async () => {
      const floaterRect = shadowFloater.getBoundingClientRect();
      const buttonRect = button.getBoundingClientRect();
      await expect(floaterRect.height).toBeCloseTo(buttonRect.height + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.width).toBeCloseTo(buttonRect.width + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.top).toBeCloseTo(buttonRect.top - OFFSET_DEFAULT, 1);
      await expect(floaterRect.left).toBeCloseTo(buttonRect.left - OFFSET_DEFAULT, 1);
      await expect(floaterRect.bottom).toBeCloseTo(buttonRect.bottom + OFFSET_DEFAULT, 1);
      await expect(floaterRect.right).toBeCloseTo(buttonRect.right + OFFSET_DEFAULT, 1);
      await expect(floaterRect.x).toBeCloseTo(buttonRect.x - OFFSET_DEFAULT, 1);
      await expect(floaterRect.y).toBeCloseTo(buttonRect.y - OFFSET_DEFAULT, 1);
    });

    // delete the button to trigger focus lost
    button.remove();
    await waitFor("should match button dimensions", async () => {
      const floaterRect = shadowFloater.getBoundingClientRect();
      await expect(floaterRect.height).toBe(0);
      await expect(floaterRect.width).toBe(0);
      await expect(floaterRect.top).toBe(0);
      await expect(floaterRect.left).toBe(0);
      await expect(floaterRect.bottom).toBe(0);
      await expect(floaterRect.right).toBe(0);
      await expect(floaterRect.x).toBe(0);
      await expect(floaterRect.y).toBe(0);
    });
  }
}

Should Respect Body Position

Story: components-floating-focus-interaction-tests--should-respect-body-position · tags: play-fn

<div>
  <button>Focus</button>
</div>

<style>
  body {
    margin: 20px !important;
  }
</style>
${visibleHost}
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <div>
        <button>Focus</button>
      </div>

      <style>
        body {
          margin: 20px !important;
        }
      </style>
      ${visibleHost}
    `;
  },
  async play({
    canvasElement
  }) {
    const ocFloatingFocus = document.body.querySelector("oc-floating-focus-v1")! as HTMLElement;
    const shadowFloater = ocFloatingFocus.shadowRoot!.querySelector(".floating-focus")! as HTMLElement;
    const button = canvasElement.querySelector("button")!;
    keyboardFocus(button);
    await waitFor("should match button dimensions", async () => {
      const floaterRect = shadowFloater.getBoundingClientRect();
      const buttonRect = button.getBoundingClientRect();
      await expect(floaterRect.height).toBeCloseTo(buttonRect.height + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.width).toBeCloseTo(buttonRect.width + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.top).toBeCloseTo(buttonRect.top - OFFSET_DEFAULT, 1);
      await expect(floaterRect.left).toBeCloseTo(buttonRect.left - OFFSET_DEFAULT, 1);
      await expect(floaterRect.bottom).toBeCloseTo(buttonRect.bottom + OFFSET_DEFAULT, 1);
      await expect(floaterRect.right).toBeCloseTo(buttonRect.right + OFFSET_DEFAULT, 1);
      await expect(floaterRect.x).toBeCloseTo(buttonRect.x - OFFSET_DEFAULT, 1);
      await expect(floaterRect.y).toBeCloseTo(buttonRect.y - OFFSET_DEFAULT, 1);
    });
  }
}

Should Respect Body Position Fixed

Story: components-floating-focus-interaction-tests--should-respect-body-position-fixed · tags: play-fn

<div>
  <button>Focus</button>
</div>

<style>
  body {
    margin: 20px !important;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
</style>
${visibleHost}
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <div>
        <button>Focus</button>
      </div>

      <style>
        body {
          margin: 20px !important;
          position: fixed;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
        }
      </style>
      ${visibleHost}
    `;
  },
  async play({
    canvasElement
  }) {
    const ocFloatingFocus = document.body.querySelector("oc-floating-focus-v1")! as HTMLElement;
    const shadowFloater = ocFloatingFocus.shadowRoot!.querySelector(".floating-focus")! as HTMLElement;
    const button = canvasElement.querySelector("button")!;
    keyboardFocus(button);
    await waitFor("should match button dimensions", async () => {
      const floaterRect = shadowFloater.getBoundingClientRect();
      const buttonRect = button.getBoundingClientRect();
      await expect(floaterRect.height).toBeCloseTo(buttonRect.height + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.width).toBeCloseTo(buttonRect.width + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.top).toBeCloseTo(buttonRect.top - OFFSET_DEFAULT, 1);
      await expect(floaterRect.left).toBeCloseTo(buttonRect.left - OFFSET_DEFAULT, 1);
      await expect(floaterRect.bottom).toBeCloseTo(buttonRect.bottom + OFFSET_DEFAULT, 1);
      await expect(floaterRect.right).toBeCloseTo(buttonRect.right + OFFSET_DEFAULT, 1);
      await expect(floaterRect.x).toBeCloseTo(buttonRect.x - OFFSET_DEFAULT, 1);
      await expect(floaterRect.y).toBeCloseTo(buttonRect.y - OFFSET_DEFAULT, 1);
    });
  }
}

Should Work Within Cinema

Story: components-floating-focus-interaction-tests--should-work-within-cinema · tags: play-fn

<oc-cinema-v1 style="width: 500px">
  <button>Item</button>
  <button>Item</button>
  <button>Item</button>
  <button>Item</button>
  <button>Item</button>
  <button>Item</button>
  <button>Item</button>
  <button>Item</button>
</oc-cinema-v1>

<style>
  body {
    margin: 20px !important;
    width: 100%;
    height: 100%;
  }
</style>
${visibleHost}
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <oc-cinema-v1 style="width: 500px">
        <button>Item</button>
        <button>Item</button>
        <button>Item</button>
        <button>Item</button>
        <button>Item</button>
        <button>Item</button>
        <button>Item</button>
        <button>Item</button>
      </oc-cinema-v1>

      <style>
        body {
          margin: 20px !important;
          width: 100%;
          height: 100%;
        }
      </style>
      ${visibleHost}
    `;
  },
  async play({
    canvasElement
  }) {
    const ocFloatingFocus = document.body.querySelector("oc-floating-focus-v1")! as HTMLElement;
    const shadowFloater = ocFloatingFocus.shadowRoot!.querySelector(".floating-focus")! as HTMLElement;
    const button = canvasElement.querySelectorAll("button").item(4);
    keyboardFocus(button);
    await waitFor("should match button dimensions", async () => {
      const floaterRect = shadowFloater.getBoundingClientRect();
      const buttonRect = button.getBoundingClientRect();
      await expect(floaterRect.height).toBeCloseTo(buttonRect.height + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.width).toBeCloseTo(buttonRect.width + OFFSET_DEFAULT * 2, 1);
      await expect(floaterRect.top).toBeCloseTo(buttonRect.top - OFFSET_DEFAULT, 1);
      await expect(floaterRect.left).toBeCloseTo(buttonRect.left - OFFSET_DEFAULT, 1);
      await expect(floaterRect.bottom).toBeCloseTo(buttonRect.bottom + OFFSET_DEFAULT, 1);
      await expect(floaterRect.right).toBeCloseTo(buttonRect.right + OFFSET_DEFAULT, 1);
      await expect(floaterRect.x).toBeCloseTo(buttonRect.x - OFFSET_DEFAULT, 1);
      await expect(floaterRect.y).toBeCloseTo(buttonRect.y - OFFSET_DEFAULT, 1);
    });
  }
}

Should Work Within Dialog

Story: components-floating-focus-interaction-tests--should-work-within-dialog · tags: play-fn

<dialog>
  <button>Item</button>
</dialog>

<style>
  body {
    margin: 20px !important;
    width: 100%;
    height: 100%;
  }

  dialog {
    border: none;
  }
</style>
${visibleHost}
Story source (TypeScript, verbatim from Storybook)
{
  render() {
    return html`
      <dialog>
        <button>Item</button>
      </dialog>

      <style>
        body {
          margin: 20px !important;
          width: 100%;
          height: 100%;
        }

        dialog {
          border: none;
        }
      </style>
      ${visibleHost}
    `;
  },
  async play({
    canvasElement
  }) {
    // skip this test in pipeline, as it seems not to work with dialogs in pipeline
    if (!import.meta.env.DEV) return;
    await expect(canUseHtmlDialogElement, "dialog should be available").toBeTruthy();
    await macrotasks(1);
    const ocFloatingFocus = document.body.querySelector("oc-floating-focus-v1")! as HTMLElement;
    const shadowFloater = ocFloatingFocus.shadowRoot!.querySelector(".floating-focus")! as HTMLElement;
    const dialog = canvasElement.querySelector("dialog")!;
    dialog.showModal();
    await macrotasks(1);
    const button = canvasElement.querySelector<HTMLButtonElement>("button")!;
    keyboardFocus(button);
    await waitFor("should match button dimensions", async () => {
      const floaterRect = shadowFloater.getBoundingClientRect();
      const buttonRect = button.getBoundingClientRect();
      await expect(floaterRect.height, "height").toBeCloseTo(buttonRect.height + 8, 1);
      await expect(floaterRect.width, "width").toBeCloseTo(buttonRect.width + 8, 1);
      await expect(floaterRect.top, "top").toBeCloseTo(buttonRect.top - 4, 1);
      await expect(floaterRect.left, "left").toBeCloseTo(buttonRect.left - 4, 1);
      await expect(floaterRect.bottom, "bottom").toBeCloseTo(buttonRect.bottom + 4, 1);
      await expect(floaterRect.right, "right").toBeCloseTo(buttonRect.right + 4, 1);
      await expect(floaterRect.x, "x").toBeCloseTo(buttonRect.x - 4, 1);
      await expect(floaterRect.y, "y").toBeCloseTo(buttonRect.y - 4, 1);
    });
  }
}