mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: unify SkipToContent across studio, docs, www, and design-system (#48314)
## What kind of change does this PR introduce? Feature / a11y polish ## What is the current behavior? Studio and Docs each had their own skip-to-content link (different styling and behaviour). www and design-system had none. ## What is the new behavior? Shared `SkipToContent` in `ui-patterns`, adopted by Studio, Docs, www, and design-system. Documented as a fragment with a short note under Accessibility → Jumping ahead. Tab once to reveal the button (top-left), Enter to jump to a content-only `<main>`. | After | | --- | | <img width="836" height="324" alt="CleanShot 2026-07-24 at 14 08 47@2x" src="https://github.com/user-attachments/assets/6df29452-e53a-4eca-8f64-946f2b9f605d" /> | ## To test Shared steps for every app: enable Tab key navigation if needed, load the preview, press **Tab** once — skip button should slide in top-left. Press **Enter** — focus jumps to main content (no blue ring on `<main>`). Press **Tab** again — first interactive control in the page body, not the sidebar/nav. Hover the skip button — solid fill, clear hover state, no chrome showing through. - **Studio** — [preview](https://studio-staging-git-dnywh-featskip-to-content-supabase.vercel.app) → sign in → any project page - **Docs** — [preview](https://docs-git-dnywh-featskip-to-content-supabase.vercel.app) → any docs page with sidebar - **www** — [preview](https://zone-www-dot-com-git-dnywh-featskip-to-content-supabase.vercel.app) → homepage or any marketing page with the default nav - **Design system** — [preview](https://design-system-git-dnywh-featskip-to-content-supabase.vercel.app) → any docs page (confirm Tab from content does **not** walk the sidebar), plus [Skip to Content fragment](https://design-system-git-dnywh-featskip-to-content-supabase.vercel.app/docs/fragments/skip-to-content) ## Additional context Follow-up to #47694 / #48303 (Studio) and #47515 (Docs). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a reusable “Skip to content” accessibility link across key layouts and pages. - Updated main landmarks to support keyboard focus and skip-link navigation (`id="main"`). - **Accessibility** - Skip links now follow consistent landmark-target conventions and remain hidden until focused. - Improved documentation for skip links/jump shortcuts in persistent chrome layouts. - **Documentation** - Added a dedicated Skip to Content fragment, navigation entry, and expanded accessibility guidance. - Updated button description wording in component docs. - **Tests** - Added component tests for SkipToContent. - **Chores** - Exposed SkipToContent via additional public package entry points. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
944b41c708
commit
37dded67d1
21 files changed
+228
-59
No files matched your search
@@ -146,7 +146,8 @@ export default function Component() {
|
||||
{['desktop', 'mobile'].map((key) => {
|
||||
const chart = key as keyof typeof chartConfig
|
||||
return (
|
||||
<button tabIndex={0}
|
||||
<button
|
||||
tabIndex={0}
|
||||
key={chart}
|
||||
data-active={activeChart === chart}
|
||||
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-surface-100 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
|
||||
|
||||
@@ -2502,6 +2502,17 @@ export const Index: Record<string, any> = {
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"skip-to-content-demo": {
|
||||
name: "skip-to-content-demo",
|
||||
type: "components:example",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/registry/default/example/skip-to-content-demo")),
|
||||
source: "",
|
||||
files: ["registry/default/example/skip-to-content-demo.tsx"],
|
||||
category: "undefined",
|
||||
subcategory: "undefined",
|
||||
chunks: []
|
||||
},
|
||||
"page-container-demo": {
|
||||
name: "page-container-demo",
|
||||
type: "components:example",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ScrollArea } from 'ui'
|
||||
import { SkipToContent } from 'ui-patterns/SkipToContent'
|
||||
|
||||
import { MobileSidebarSheet } from '@/components/mobile-sidebar-sheet'
|
||||
import { SideNavigation } from '@/components/side-navigation'
|
||||
@@ -12,18 +13,22 @@ interface AppLayoutProps {
|
||||
export default async function AppLayout({ children }: AppLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SkipToContent href="#main" />
|
||||
<TopNavigation />
|
||||
<MobileSidebarSheet />
|
||||
<main className="flex-1 max-w-site mx-auto w-full border-l border-r border-b">
|
||||
<div className="flex-1 max-w-site mx-auto w-full border-l border-r border-b">
|
||||
<div className="flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] lg:grid-cols-[240px_minmax(0,1fr)]">
|
||||
<aside className="fixed top-10 z-30 hidden h-[calc(100vh-3rem)] w-full shrink-0 md:sticky md:block border-r">
|
||||
<ScrollArea className="h-full">
|
||||
<SideNavigation />
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
{children}
|
||||
{/* Content-only landmark: sidebar must stay outside so skip/Tab don't land in the nav */}
|
||||
<main id="main" tabIndex={-1} className="outline-hidden scroll-mt-12 min-w-0">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<SiteFooter />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -247,6 +247,11 @@ export const docsConfig: DocsConfig = {
|
||||
href: '/docs/fragments/single-value-field-array',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Skip to Content',
|
||||
href: '/docs/fragments/skip-to-content',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -116,11 +116,13 @@ Individual options inside of a group can be reached by arrow keys (↑ ↓ ←
|
||||
|
||||
### Jumping ahead
|
||||
|
||||
Some keyboard-navigable content may be contain hundreds or thousands of items. Help users jump to specific content with the following mitigation strategies:
|
||||
Some keyboard-navigable content may contain hundreds or thousands of items. Help users jump to specific content with:
|
||||
|
||||
- Search and filtering
|
||||
- Pagination or virtualization
|
||||
- “Jump to” shortcuts to skip ahead
|
||||
- Skip links and “jump to” shortcuts
|
||||
|
||||
Apps with persistent header and sidebar chrome should expose a skip link as the first focusable element. Use the shared [Skip to Content](fragments/skip-to-content) fragment which owns the component API, usage sample, and target landmark contract.
|
||||
|
||||
## Screen readers
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Button
|
||||
description: Displays a button or a component that looks like a button.
|
||||
description: Displays a button or a link that looks like a button.
|
||||
featured: true
|
||||
component: true
|
||||
---
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Skip to Content
|
||||
description: Keyboard-accessible skip link that jumps past chrome to the main landmark.
|
||||
component: true
|
||||
fragment: true
|
||||
---
|
||||
|
||||
<ComponentPreview name="skip-to-content-demo" peekCode wide />
|
||||
|
||||
Keyboard-accessible skip link composed from [Button](../components/button) for apps with persistent header and sidebar chrome. Hidden until focused via Tab, then slides into view so users can jump past navigation to the main landmark.
|
||||
|
||||
The preview above shows the focused appearance. In product apps, the link stays off-screen until keyboard focus.
|
||||
|
||||
See [Accessibility](../accessibility#jumping-ahead) for more information on when to use skip links, and how they coexist alongside other navigation aids.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { SkipToContent } from 'ui-patterns/SkipToContent'
|
||||
```
|
||||
|
||||
```tsx
|
||||
<SkipToContent href="#main" />
|
||||
|
||||
<main id="main" tabIndex={-1} className="scroll-mt-(--header-height) outline-hidden">
|
||||
{children}
|
||||
</main>
|
||||
```
|
||||
|
||||
Place the skip link at the root of the app chrome so Tab reaches it before navigation. The target landmark must be content only. Do not wrap a sidebar inside the same `<main>`, otherwise a Tab after skip will land in the sidebar navigation.
|
||||
|
||||
## Props
|
||||
|
||||
### `href`
|
||||
|
||||
Hash href to the main content landmark, e.g. `#main`.
|
||||
|
||||
### `children`
|
||||
|
||||
Link label. Defaults to `Skip to content`.
|
||||
|
||||
### `className`
|
||||
|
||||
Optional classes merged onto the positioning wrapper (useful for demos or layout overrides).
|
||||
|
||||
## Target landmark
|
||||
|
||||
Callers own the landmark the skip link points at:
|
||||
|
||||
- Matching `id`
|
||||
- `tabIndex={-1}` so Enter moves focus onto the landmark
|
||||
- `scroll-mt` when a sticky header is present
|
||||
- `outline-hidden` so the landmark has no visible focus ring. The subsequent Tab will land on the first interactive child
|
||||
@@ -7,11 +7,13 @@ Supabase has a necessarily complex navigation system to handle multiple products
|
||||
|
||||
## Components
|
||||
|
||||
### NavMenu
|
||||
### [Nav Menu](../components/nav-menu)
|
||||
|
||||
A horizontal list of related views within a consistent PageLayout context, allowing for clearer page-level organisation. Activating a NavMenu item should trigger a URL change.
|
||||
|
||||
[NavMenu component guidelines](../components/nav-menu)
|
||||
### [Skip To Content](../fragments/skip-to-content)
|
||||
|
||||
Keyboard-accessible skip link for apps with persistent header and sidebar chrome. Hidden until focused via Tab, then slides into view so users can jump past navigation to the main landmark. See [Accessibility](../accessibility#jumping-ahead) for broader context.
|
||||
|
||||
## Page titles
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SkipToContent } from 'ui-patterns/SkipToContent'
|
||||
|
||||
export default function SkipToContentDemo() {
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden rounded-md border bg-studio">
|
||||
<div className="flex items-center border-b px-4 py-3 text-sm text-foreground-muted">
|
||||
Demo header / navigation
|
||||
</div>
|
||||
{/* Preview shows the focused appearance; production hides until Tab */}
|
||||
<SkipToContent href="#skip-demo-main" className="relative left-3 top-2 translate-y-0" />
|
||||
<main id="skip-demo-main" tabIndex={-1} className="outline-hidden p-6 pt-2">
|
||||
<p className="text-sm text-foreground-light">
|
||||
Main content landmark. In a real app, Tab once to reveal the skip link, then Enter to jump
|
||||
here.
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1351,6 +1351,11 @@ export const examples: Registry = [
|
||||
type: 'components:example',
|
||||
files: ['example/info-tooltip-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'skip-to-content-demo',
|
||||
type: 'components:example',
|
||||
files: ['example/skip-to-content-demo.tsx'],
|
||||
},
|
||||
{
|
||||
name: 'page-container-demo',
|
||||
type: 'components:example',
|
||||
|
||||
Reference in new issue
Block a user