Skip-link component
components specs/components/skip-link.kmd
The keyboard-only shortcut that lets a user jump straight past repeated chrome (topbar, nav) to the page's real content. Visually absent until it takes keyboard focus, then it slides into view. Multiple targets (content, navigation, search) may be offered, each with an optional shortcut-key hint. Required by WCAG 2.4.1 (Bypass Blocks) and eMAG 3.1; the gov.br DS ships it as `.br-skiplink`, and KDS had focus-ring/keyboard support but no component.
Quando esta spec se aplica
Triggers primários
- Render a skip link / bypass-blocks shortcut
Todos os triggers
- Add a 'skip to content' / 'pular para o conteúdo' link
- Let keyboard users bypass the topbar/nav (WCAG 2.4.1 Bypass Blocks)
- Meet eMAG 3.1 for a gov.br surface
Corpo da especificação
Component — Skip link
Status: v0.1.0 — Draft. Opened from the KDS × gov.br gap analysis (
tools/design-gen/docs/govbr-ds-gap-analysis.md, stack#395): gov.br shipsskiplink(eMAG); KDS had focus-ring + keyboard nav but no skip-link component. Behaviour below is MEASURED from@govbr-ds/coredist/core.min.cssv3.7 (.br-skiplink, 8 occurrences), not inferred. Normative basis: WCAG 2.4.1 (Bypass Blocks, level A) + eMAG 3.1 (Recomendação 1.5 — "fornecer âncora para ir direto a um bloco de conteúdo").
Why it exists
A keyboard or screen-reader user meets the same topbar and navigation on every page. Without a bypass they tab through all of it, every time, before reaching what they came for. The skip link is the standard escape hatch — and it is the one component whose whole value is that sighted mouse users never see it.
R1 — Hidden off-screen, NEVER display: none
The link is present in the DOM and in the tab order at all times, and hidden
by moving it out of the viewport — gov.br uses top: -100vh on a
position: absolute anchor inside a position: fixed container.
display: none / visibility: hidden / hidden are FORBIDDEN here, and
this is the rule the whole component hangs on: those remove the element from the
tab order, so it can never receive the focus that is supposed to reveal it. A
skip link you cannot tab to is not a degraded skip link — it is a decoration
that silently fails the criterion it exists to satisfy.
Equally forbidden: rendering it only on :hover, or gating it behind a
media query — a keyboard user has no pointer, and the need is not viewport-sized.
R2 — Focus reveals it
On :focus and :focus-visible the link returns to the viewport
(gov.br: top: 0, transition 150ms ease-out). On blur it leaves again.
Both pseudo-classes are asserted on purpose: :focus-visible alone would let a
UA that withholds the heuristic hide the link from a real keyboard user, and
:focus alone re-shows it on a programmatic focus that no human drove.
R3 — Multiple targets
The container is a column (gov.br: flex-direction: column), so a surface MAY
offer several bypasses — typically content, navigation, search — in
that order (most-wanted first). Each is an anchor to a real landmark id on the
page; a skip link pointing at an id that does not exist is a broken promise and
MUST fail the surface's a11y gate rather than silently no-op.
R4 — Shortcut hint
Each link MAY carry a shortcut-key hint rendered as a tag (gov.br nests
.br-tag with background: var(--interactive); the KDS counterpart is chips.kmd).
The hint is decorative: the
link MUST work by plain tab-and-enter with no shortcut bound.
R5 — Geometry + placement (per active DS)
position: fixed, pinned to the top-left of the viewport, above all chrome.
gov.br's measured values: left: --spacing-scale-2x (16px), min-width: 300px,
z-index: --z-index-layer-4, box-shadow: --surface-shadow-lg, background
--background.
Per design-RFC-014, a Koder surface takes these from the active design
system, not from hardcoded constants — the same rule the rest of the kit now
follows (theme::Shape / theme::Palette, kroma#189/#278). The skip link is
chrome, so it sits above the topbar in stacking order
(specs/app-layout/topbar-placement.kmd).
R6 — Accessibility
- The link text is a real, translated sentence ("Pular para o conteúdo" /
"Skip to content") — never an icon or an abbreviation
(
specs/i18n/contract.kmd; theen-US+pt-BRfloor applies). - It is the first focusable element on the page. A skip link you reach after the topbar has bypassed nothing.
- Activating it moves focus to the target landmark, not merely the scroll position — a screen reader that scrolls but does not move focus keeps reading the nav.
R7 — Test
A surface claiming this component MUST prove, headlessly:
- the link is focusable (tab from page load reaches it first) — this is the
negative control for R1: a
display:noneregression fails here and nowhere else; - focusing it brings it into the viewport (R2), and blurring removes it;
- every target id resolves to a landmark on the page (R3);
- activation moves focus to that landmark (R6).
Asserting only (2) is the trap: a link that is visible-on-focus but unreachable by tab passes the eye test and fails the user.
Cross-link
specs/accessibility/conformance-br.kmd— eMAG 3.1 obligations for BR surfaces.specs/app-layout/topbar-placement.kmd— the chrome it must sit above and bypass.specs/components/chips.kmd— the shortcut-hint chip/tag (R4).- design-RFC-014 — the active-DS geometry/colour source (R5).
Requirements (testable)
Requirement: Present in the DOM and tab order at all times {#req-skip-link-dom-presence}
The skip link SHALL remain present in the DOM and reachable in the tab order at all times, hidden only by being moved out of the viewport; it SHALL NOT be hidden with display: none, visibility: hidden, the hidden attribute, a :hover-only rule, or a media query, because any of those removes it from the tab order it depends on.
Scenario: First tab from page load reaches the link
- GIVEN a page that declares the skip-link component
- WHEN the page loads and the user presses Tab once with no prior focus
- THEN the skip link receives keyboard focus as the first focusable element
- AND its computed style is not
display: noneand notvisibility: hidden
Scenario: Link stays in tab order while off-screen
- GIVEN the skip link rendered but not focused
- WHEN its computed geometry is inspected
- THEN it is positioned outside the viewport (e.g.
top: -100vh) rather than removed from layout - AND it still appears in the document's sequential tab order
Requirement: Focus reveals the link, blur hides it {#req-skip-link-focus-reveal}
The skip link SHALL return into the viewport on both :focus and :focus-visible, and SHALL leave the viewport again on blur, so that a keyboard user always sees it when it holds focus and a mouse user never does.
Scenario: Focusing brings the link into view
- GIVEN the skip link off-screen and unfocused
- WHEN it receives keyboard focus
- THEN it animates back into the viewport (e.g.
top: 0) - AND its bounding box is fully within the viewport
Scenario: Blurring hides the link again
- GIVEN the skip link currently focused and visible in the viewport
- WHEN focus moves away from it
- THEN it returns to its off-screen position
- AND its bounding box is outside the viewport
Requirement: Every declared target resolves to a real landmark {#req-skip-link-target-resolution}
When a surface offers one or more bypasses (typically content, navigation, search, in that order), each link SHALL point at the id of a real landmark that exists on the page; a link whose target id does not resolve SHALL fail the surface's accessibility gate rather than silently no-op.
Scenario: All target ids resolve
- GIVEN a skip-link container offering content, navigation, and search bypasses
- WHEN the a11y gate resolves each link's target id against the DOM
- THEN every referenced id matches an existing landmark element on the page
Scenario: A dangling target fails the gate
- GIVEN a skip link whose href points at an id absent from the page
- WHEN the surface's accessibility gate runs
- THEN the gate reports a failure for the unresolved target
- AND the surface is not allowed to pass
Requirement: Activation moves focus to the target landmark {#req-skip-link-focus-move}
Activating a skip link SHALL move keyboard focus to the target landmark, not merely change scroll position, so that a screen reader resumes reading at the bypassed content instead of the chrome.
Scenario: Enter moves focus past the chrome
- GIVEN the skip link focused as the first element on the page
- WHEN the user activates it with Enter
- THEN keyboard focus moves to the referenced target landmark
- AND the active element is inside that landmark, not the topbar or nav
Requirement: Works by plain tab-and-enter without a shortcut {#req-skip-link-shortcut-optional}
Each link MAY carry a decorative shortcut-key hint rendered as a tag/chip, but it SHALL operate by plain tab-and-enter with no shortcut key bound; the hint SHALL NOT be required to reach or activate the link, and the link text SHALL be a real translated sentence rather than an icon or abbreviation.
Scenario: No shortcut binding is needed to use the link
- GIVEN a skip link that displays a shortcut-key hint chip
- WHEN the user reaches it by Tab and presses Enter without pressing any shortcut key
- THEN focus moves to the target landmark
- AND the link text is a full translated sentence (e.g. "Skip to content" / "Pular para o conteúdo")
Referências
specs/accessibility/conformance-br.kmdspecs/app-layout/topbar-placement.kmdspecs/navigation/back-behavior.kmdspecs/components/chips.kmd