diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts
index c0378ab47e..c5f83bb3c1 100644
--- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts
+++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts
@@ -377,6 +377,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'The search bar component of your site, appearing in the navbar.',
},
+ SkipToContent: {
+ actions: {
+ eject: 'safe',
+ wrap: 'safe',
+ },
+ description:
+ 'The component responsible for implementing the accessibility "skip to content" link (https://www.w3.org/TR/WCAG20-TECHS/G1.html)',
+ },
'prism-include-languages': {
actions: {
eject: 'safe',
diff --git a/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx
index 26308f7eb2..cf46036476 100644
--- a/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/Layout/index.tsx
@@ -8,7 +8,11 @@
import React from 'react';
import clsx from 'clsx';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
-import {PageMetadata, ThemeClassNames} from '@docusaurus/theme-common';
+import {
+ PageMetadata,
+ SkipToContentFallbackId,
+ ThemeClassNames,
+} from '@docusaurus/theme-common';
import {useKeyboardNavigation} from '@docusaurus/theme-common/internal';
import SkipToContent from '@theme/SkipToContent';
import AnnouncementBar from '@theme/AnnouncementBar';
@@ -42,6 +46,7 @@ export default function Layout(props: Props): JSX.Element {
- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
-
-
- Skip to main content
-
-
-
- );
+ return ;
}
diff --git a/packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts b/packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts
deleted file mode 100644
index bdcac465d4..0000000000
--- a/packages/docusaurus-theme-common/src/hooks/useSkipToContent.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import type React from 'react';
-import {useCallback, useRef} from 'react';
-import {useHistory} from '@docusaurus/router';
-import {useLocationChange} from '../utils/useLocationChange';
-import {ThemeClassNames} from '../utils/ThemeClassNames';
-
-function programmaticFocus(el: HTMLElement) {
- el.setAttribute('tabindex', '-1');
- el.focus();
- el.removeAttribute('tabindex');
-}
-
-/** This hook wires the logic for a skip-to-content link. */
-export function useSkipToContent(): {
- /**
- * The ref to the container. On page transition, the container will be focused
- * so that keyboard navigators can instantly interact with the link and jump
- * to content. **Note:** the type is `RefObject` only because
- * the typing for refs don't reflect that the `ref` prop is contravariant, so
- * using `HTMLElement` causes type-checking to fail. You can plug the ref into
- * any HTML element, as long as it can be focused.
- */
- containerRef: React.RefObject;
- /**
- * Callback fired when the skip to content link has been interacted with. It
- * will programmatically focus the main content.
- */
- handleSkip: (e: React.MouseEvent) => void;
-} {
- const containerRef = useRef(null);
- const {action} = useHistory();
- const handleSkip = useCallback((e: React.MouseEvent) => {
- e.preventDefault();
-
- const targetElement: HTMLElement | null =
- document.querySelector('main:first-of-type') ??
- document.querySelector(`.${ThemeClassNames.wrapper.main}`);
-
- if (targetElement) {
- programmaticFocus(targetElement);
- }
- }, []);
-
- useLocationChange(({location}) => {
- if (containerRef.current && !location.hash && action === 'PUSH') {
- programmaticFocus(containerRef.current);
- }
- });
-
- return {containerRef, handleSkip};
-}
diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts
index d282c1d853..03503ddfc1 100644
--- a/packages/docusaurus-theme-common/src/index.ts
+++ b/packages/docusaurus-theme-common/src/index.ts
@@ -81,4 +81,9 @@ export {useDocsPreferredVersion} from './contexts/docsPreferredVersion';
export {processAdmonitionProps} from './utils/admonitionUtils';
+export {
+ SkipToContentFallbackId,
+ SkipToContentLink,
+} from './utils/skipToContentUtils';
+
export {ErrorBoundaryTryAgainButton} from './utils/errorBoundaryUtils';
diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts
index 8415cc967b..aefaa2ddba 100644
--- a/packages/docusaurus-theme-common/src/internal.ts
+++ b/packages/docusaurus-theme-common/src/internal.ts
@@ -117,6 +117,5 @@ export {
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
export {useSearchPage} from './hooks/useSearchPage';
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
-export {useSkipToContent} from './hooks/useSkipToContent';
export {getPrismCssVariables} from './utils/codeBlockUtils';
export {useBackToTopButton} from './hooks/useBackToTopButton';
diff --git a/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx b/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx
new file mode 100644
index 0000000000..fbdf862a8c
--- /dev/null
+++ b/packages/docusaurus-theme-common/src/utils/skipToContentUtils.tsx
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, {useCallback, useRef, type ComponentProps} from 'react';
+import {useHistory} from '@docusaurus/router';
+import {translate} from '@docusaurus/Translate';
+import {useLocationChange} from './useLocationChange';
+
+/**
+ * The id of the element that should become focused on a page
+ * that does not have a html tag.
+ * Focusing the Docusaurus Layout children is a reasonable fallback.
+ */
+export const SkipToContentFallbackId = 'docusaurus_skipToContent_fallback';
+
+/**
+ * Returns the skip to content element to focus when the link is clicked.
+ */
+function getSkipToContentTarget(): HTMLElement | null {
+ return (
+ // Try to focus the in priority
+ // Note: this will only work if JS is enabled
+ // See https://github.com/facebook/docusaurus/issues/6411#issuecomment-1284136069
+ document.querySelector('main:first-of-type') ??
+ // Then try to focus the fallback element (usually the Layout children)
+ document.getElementById(SkipToContentFallbackId)
+ );
+}
+
+function programmaticFocus(el: HTMLElement) {
+ el.setAttribute('tabindex', '-1');
+ el.focus();
+ el.removeAttribute('tabindex');
+}
+
+/** This hook wires the logic for a skip-to-content link. */
+function useSkipToContent(): {
+ /**
+ * The ref to the container. On page transition, the container will be focused
+ * so that keyboard navigators can instantly interact with the link and jump
+ * to content.
+ */
+ containerRef: React.RefObject;
+ /**
+ * Callback fired when the skip to content link has been clicked.
+ * It will programmatically focus the main content.
+ */
+ onClick: (e: React.MouseEvent) => void;
+} {
+ const containerRef = useRef(null);
+ const {action} = useHistory();
+
+ const onClick = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ const targetElement = getSkipToContentTarget();
+ if (targetElement) {
+ programmaticFocus(targetElement);
+ }
+ }, []);
+
+ // "Reset" focus when navigating.
+ // See https://github.com/facebook/docusaurus/pull/8204#issuecomment-1276547558
+ useLocationChange(({location}) => {
+ if (containerRef.current && !location.hash && action === 'PUSH') {
+ programmaticFocus(containerRef.current);
+ }
+ });
+
+ return {containerRef, onClick};
+}
+
+const DefaultSkipToContentLabel = translate({
+ id: 'theme.common.skipToMainContent',
+ description:
+ 'The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation',
+ message: 'Skip to main content',
+});
+
+type SkipToContentLinkProps = Omit, 'href' | 'onClick'>;
+
+export function SkipToContentLink(props: SkipToContentLinkProps): JSX.Element {
+ const linkLabel = props.children ?? DefaultSkipToContentLabel;
+ const {containerRef, onClick} = useSkipToContent();
+ return (
+
+ );
+}