diff --git a/superset-frontend/src/components/Accessibility/SkipLink.test.tsx b/superset-frontend/src/components/Accessibility/SkipLink.test.tsx
new file mode 100644
index 000000000000..8951904b97f9
--- /dev/null
+++ b/superset-frontend/src/components/Accessibility/SkipLink.test.tsx
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import userEvent from '@testing-library/user-event';
+import { render, screen } from 'spec/helpers/testing-library';
+import SkipLink from './SkipLink';
+
+const createTarget = (id: string) => {
+ const target = document.createElement('div');
+ target.id = id;
+ document.body.appendChild(target);
+ return target;
+};
+
+afterEach(() => {
+ document.body.replaceChildren();
+});
+
+test('renders the default label', () => {
+ render();
+ expect(screen.getByText('Skip to main content')).toBeInTheDocument();
+});
+
+test('renders custom children when provided', () => {
+ render(Jump to content);
+ expect(screen.getByText('Jump to content')).toBeInTheDocument();
+});
+
+test('href targets the default main-content id', () => {
+ render();
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute('href', '#main-content');
+});
+
+test('href reflects custom targetId', () => {
+ render(Skip);
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute('href', '#my-target');
+});
+
+test('focuses the target element on click', async () => {
+ const target = createTarget('target-area');
+ render(Skip);
+
+ const link = screen.getByRole('link');
+ await userEvent.click(link);
+
+ expect(document.activeElement).toBe(target);
+});
+
+test('does not leave a permanent tabindex on the target after blur', async () => {
+ const target = createTarget('target-area');
+ render(Skip);
+
+ const link = screen.getByRole('link');
+ await userEvent.click(link);
+
+ expect(target).toHaveAttribute('tabindex', '-1');
+
+ target.dispatchEvent(new FocusEvent('blur'));
+ expect(target.hasAttribute('tabindex')).toBe(false);
+});
diff --git a/superset-frontend/src/components/Accessibility/SkipLink.tsx b/superset-frontend/src/components/Accessibility/SkipLink.tsx
new file mode 100644
index 000000000000..2692c625be2c
--- /dev/null
+++ b/superset-frontend/src/components/Accessibility/SkipLink.tsx
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { FC, MouseEvent } from 'react';
+import { styled } from '@apache-superset/core/theme';
+import { t } from '@apache-superset/core/translation';
+
+/**
+ * SkipLink - WCAG 2.4.1 Bypass Blocks
+ * Allows keyboard users to skip navigation and jump directly to main content.
+ * The link is visually hidden but becomes visible when focused.
+ */
+
+const StyledSkipLink = styled.a`
+ position: absolute;
+ top: -100px;
+ left: 0;
+ background: ${({ theme }) => theme.colorPrimary};
+ color: ${({ theme }) => theme.colorWhite};
+ padding: ${({ theme }) => theme.sizeUnit * 3}px
+ ${({ theme }) => theme.sizeUnit * 4}px;
+ z-index: 10000;
+ text-decoration: none;
+ font-weight: ${({ theme }) => theme.fontWeightStrong};
+ font-size: ${({ theme }) => theme.fontSizeSM}px;
+ border-radius: 0 0 ${({ theme }) => theme.borderRadius}px
+ ${({ theme }) => theme.borderRadius}px;
+ transition: top 0.2s ease-in-out;
+
+ &:focus,
+ &:focus-visible {
+ top: 0 !important;
+ outline: 3px solid ${({ theme }) => theme.colorPrimaryBorderHover};
+ outline-offset: 2px;
+ }
+
+ &:hover {
+ background: ${({ theme }) => theme.colorPrimaryHover};
+ }
+`;
+
+interface SkipLinkProps {
+ targetId?: string;
+ children?: React.ReactNode;
+}
+
+const SkipLink: FC = ({
+ targetId = 'main-content',
+ children = t('Skip to main content'),
+}) => {
+ const handleClick = (e: MouseEvent) => {
+ e.preventDefault();
+ const el = document.getElementById(targetId);
+ if (el) {
+ // Temporarily set tabindex to allow programmatic focus,
+ // then remove it on blur so the element stays in the natural tab order
+ const hadTabindex = el.hasAttribute('tabindex');
+ if (!hadTabindex) {
+ el.setAttribute('tabindex', '-1');
+ }
+ el.focus();
+ if (!hadTabindex) {
+ el.addEventListener(
+ 'blur',
+ () => el.removeAttribute('tabindex'),
+ { once: true },
+ );
+ }
+ } else {
+ window.location.hash = `#${targetId}`;
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default SkipLink;
diff --git a/superset-frontend/src/components/Accessibility/VisuallyHidden.test.tsx b/superset-frontend/src/components/Accessibility/VisuallyHidden.test.tsx
new file mode 100644
index 000000000000..a5708e7fd1b4
--- /dev/null
+++ b/superset-frontend/src/components/Accessibility/VisuallyHidden.test.tsx
@@ -0,0 +1,54 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen } from 'spec/helpers/testing-library';
+import VisuallyHidden from './VisuallyHidden';
+
+test('renders children', () => {
+ render(Screen-reader only text);
+ expect(screen.getByText('Screen-reader only text')).toBeInTheDocument();
+});
+
+test('renders as span by default', () => {
+ render(Default tag);
+ const el = screen.getByText('Default tag');
+ expect(el.tagName).toBe('SPAN');
+});
+
+test('renders as a custom element when as prop is provided', () => {
+ render(Page heading);
+ const el = screen.getByText('Page heading');
+ expect(el.tagName).toBe('H1');
+});
+
+test('forwards id and className props', () => {
+ render(
+
+ Text
+ ,
+ );
+ const el = screen.getByText('Text');
+ expect(el).toHaveAttribute('id', 'my-id');
+ expect(el).toHaveClass('my-class');
+});
+
+test('applies clip-rect style for screen-reader-only behavior', () => {
+ render(Hidden);
+ const el = screen.getByText('Hidden');
+ expect(el).toHaveStyle({ position: 'absolute' });
+});
diff --git a/superset-frontend/src/components/Accessibility/VisuallyHidden.tsx b/superset-frontend/src/components/Accessibility/VisuallyHidden.tsx
new file mode 100644
index 000000000000..c827bb9dbce5
--- /dev/null
+++ b/superset-frontend/src/components/Accessibility/VisuallyHidden.tsx
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ElementType, FC, ReactNode } from 'react';
+import { styled } from '@apache-superset/core/theme';
+
+/**
+ * VisuallyHidden — content that is available to assistive technology but not
+ * visually rendered. Use for screen-reader-only headings, labels, and live
+ * regions where a duplicate visible element would be redundant.
+ *
+ * Renders a `` by default. Pass `as` to render a different element
+ * (e.g. `as="h1"` for an sr-only page heading).
+ */
+const HiddenElement = styled.span`
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+`;
+
+export interface VisuallyHiddenProps {
+ /** Element type to render. Defaults to `'span'`. */
+ as?: ElementType;
+ children?: ReactNode;
+ id?: string;
+ className?: string;
+}
+
+const VisuallyHidden: FC = ({
+ as = 'span',
+ children,
+ ...rest
+}) => (
+
+ {children}
+
+);
+
+export default VisuallyHidden;
diff --git a/superset-frontend/src/components/Accessibility/index.tsx b/superset-frontend/src/components/Accessibility/index.tsx
new file mode 100644
index 000000000000..673640a8470b
--- /dev/null
+++ b/superset-frontend/src/components/Accessibility/index.tsx
@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export { default as SkipLink } from './SkipLink';
+export {
+ default as VisuallyHidden,
+ type VisuallyHiddenProps,
+} from './VisuallyHidden';
diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx
index 270a344d817f..f27e6b03a91f 100644
--- a/superset-frontend/src/views/App.tsx
+++ b/superset-frontend/src/views/App.tsx
@@ -28,6 +28,7 @@ import { css } from '@apache-superset/core/theme';
import { Layout, Loading } from '@superset-ui/core/components';
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
import { ErrorBoundary } from 'src/components';
+import { SkipLink } from 'src/components/Accessibility';
import Menu from 'src/features/home/Menu';
import getBootstrapData, { applicationRoot } from 'src/utils/getBootstrapData';
import ToastContainer from 'src/components/MessageToasts/ToastContainer';
@@ -75,6 +76,7 @@ const App = () => (
+