StartFoundationsComponentsContent

Focus

Keyboard focus always shows. A field lights up its own border; every other control gets a ring around it.

GuideTokens
Save Remember me Notifications
<tk-button>Save</tk-button>
<tk-checkbox>Remember me</tk-checkbox>
<tk-switch>Notifications</tk-switch>

Press Tab to move through the examples on this page. Focus shows for the keyboard (:focus-visible), not after a mouse click.

Fields: the border

A field that takes text, such as an input, select or date picker, has no ring. On focus its border turns to the focus color and thickens 0.5px inward, so nothing on the page moves.

<tk-field label="Email address">
  <tk-input type="email" name="email"></tk-input>
</tk-field>
<tk-field label="Promo code" error="This code has expired">
  <tk-input name="promo" value="SPRING"></tk-input>
</tk-field>
State Border
rest border/default, 1px
hover border/strong, 1px
focus border/accent/focus, 1.5px
error border/error, 1px
error + focus border/error, 1.5px
read-only border/subtle, 1px; still shows focus
disabled border/disabled, 1px; can't take focus
  • An error stays red on focus. The border thickens, so the user sees where they are typing and what is wrong.
  • Focus wins over hover. A focused field keeps its focus border under the pointer.

Everything else: the ring

Buttons, links, checkboxes, tabs, sliders, cards and every other control get a 2px ring, 2px away from their edge. The gap lets the page show through, so the ring stays visible on a filled control.

Publish Delete
<tk-button variant="primary">Publish</tk-button>
<tk-button tone="danger">Delete</tk-button>
<tk-icon-button label="Settings"><tk-icon name="settings"></tk-icon></tk-icon-button>

The ring is the same color on every variant and tone: a danger button's ring isn't red. It says where the keyboard is, not what the control means.

Focus on your own controls

Use the tokens and the native outline:

.my-control:focus-visible {
  outline: var(--focus-ring-width) solid var(--border-accent-focus);
  outline-offset: var(--focus-ring-offset);
}
  • Use :focus-visible, not :focus. One exception: the active item of a menu or list under roving focus may use :focus for its highlight, since it must show after a click too.
  • Never remove an outline without a replacement.
  • Use the tokens, not 2px, so your controls match.

The ring's corner

An outline follows the element's border-radius by itself, so don't set a radius on a ring. Its corner is the control's radius plus the 2px offset: an 8px control rings at 10px, a 12px card at 14px. If you must draw a ring with box-shadow, set that radius yourself: calc(var(--radius-8) + var(--focus-ring-offset)).

The inset ring

Draw the ring inside the control when an outer ring won't fit:

  • a thin handle, where the ring would be bigger than the target;
  • a scroll region, a table or a code block, whose edge is the component's edge, so a parent with overflow would clip an outer ring;
  • a cell in a dense grid, such as a calendar day, where it would overlap its neighbors;
  • a small button inside a field, where an outer ring would trace the field's border.
.my-handle:focus-visible {
  outline: var(--focus-ring-width) solid var(--border-accent-focus);
  outline-offset: calc(var(--focus-ring-offset) * -1);
}

Negate the offset token, not the width token. An inset ring's corner is the control's own.

Rings that get covered

An outline is painted with its element, and two things can erase it:

  • a positioned child (sticky, absolute, even relative), such as a sticky header, paints over the ring;
  • a mask, opacity or filter on the element fades the ring with it.

The fix: draw the ring on an empty sibling that covers the region, and make the region a stacking context.

.region { position: relative; }
.region > .scroll { isolation: isolate; }
.region > .ring { position: absolute; inset: 0; pointer-events: none; }
.scroll:focus-visible { outline: none; }
.scroll:focus-visible ~ .ring {
  outline: var(--focus-ring-width) solid var(--border-accent-focus);
  outline-offset: calc(var(--focus-ring-offset) * -1);
}

Thykes scroll regions (Scroll area, Table, Data table, Code block, Sidebar) already work this way.

Inverse surfaces

On an inverse surface (surface/inverse: dark in the light theme, light in the dark one), the focus color is too faint to see. Point the focus token at border/inverse on the surface, and every control inside it, including ones you slot in, rings in the inverse color:

.my-inverse-panel { --border-accent-focus: var(--border-inverse); }

Toasts and loud banners already do this, and tk-link takes color="inverse".

A new version is available. Reload
<tk-banner treatment="loud" label="Update" dismissible>
  A new version is available.
  <tk-button slot="actions" size="small">Reload</tk-button>
</tk-banner>

Forced colors

In Windows High Contrast Mode (forced-colors: active) the browser removes box-shadow and paints every border in one system color, so a field's own focus border disappears. Thykes fields add a real outline there, in the system Highlight color. Rings are outlines already and need nothing. If you build your own field, do the same:

@media (forced-colors: active) {
  .my-field:focus-within {
    outline: var(--focus-ring-width) solid Highlight;
    outline-offset: var(--focus-ring-offset);
  }
}

Accessibility

Built in:

  • A visible focus indicator on every focusable part, for keyboard users.
  • Focus never moves the layout: the field border thickens inward, and an outline takes no space.
  • An indicator that survives Windows High Contrast Mode.
  • Read-only fields take focus and show it; disabled ones don't take focus.

Your part:

  • Keep the indicator on anything you style or build; never outline: none alone.
  • On an inverse surface you build, point --border-accent-focus at border/inverse.
  • Don't let a sticky child, mask or overflow hide a ring; check it with the keyboard.

Contrast limits

The ring uses the primary color (primary/500 in light and dark, primary/800 in high contrast), so its contrast depends on the primary you pick. The non-text minimum is 3:1.

  • Light: blue, indigo and purple pass on every surface. Orange fails on every surface (2.20:1 on white). Gray fails on surface/subtle (2.9993:1) and surface/sunken; green, mint, teal and brown fail on surface/sunken.
  • Dark: only orange passes everywhere. Blue is 2.38:1 on surface/subtle and 3.32:1 on surface/raised, the panel of every dialog, menu and popover. Indigo fails on both.
  • High contrast: passes for every hue on every surface.

If focus must be clear for everyone, offer the high-contrast theme.

Do / Don't

✅ Do ❌ Don't
Let a field's border show its focus Add a ring and a border change to a field
Use outline with the focus tokens Write outline: 2px solid …
Keep the error red on a focused invalid field Let the focus color replace the error color
Use :focus-visible Use bare :focus for a ring
Negate --focus-ring-offset for an inset ring Negate --focus-ring-width
Draw the ring on a sibling overlay when a child is positioned Draw it on the element that contains the sticky child
Point the focus token at border/inverse on an inverse surface Leave a primary ring on an inverse surface