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