Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"react-error-boundary": "6.1.1",
"react-hook-form": "^7.73.1",
"react-is": "^19.2.5",
"react-router": "7.14.2",
"remark": "^15.0.1",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
Expand Down
7 changes: 7 additions & 0 deletions docs/scripts/reportBrokenLinks.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ async function main() {
ignoredPaths: [],
// CSS selectors for content to ignore during link checking
ignoredContent: [],
ignores: [
{
// React Router demo for Tabs component
path: '/react/components/tabs',
href: ['/overview', '/projects', '/account'],
},
],
});

process.exit(issues.length);
Expand Down
6 changes: 5 additions & 1 deletion docs/src/app/(docs)/react/components/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,7 @@ A component for toggling between related panels on the same page.
- Root
- List
- Tab
- LinkTab
- Indicator
- Panel
- Exports:
Expand All @@ -1717,7 +1718,10 @@ A component for toggling between related panels on the same page.
- Tabs - Tab
- Props: className, disabled, nativeButton, render, style, value
- Data Attributes: data-activation-direction, data-active, data-disabled, data-orientation
- Types: Tabs.Indicator.Props, Tabs.Indicator.State, Tabs.List.Props, Tabs.List.State, Tabs.Panel.Metadata, Tabs.Panel.Props, Tabs.Panel.State, Tabs.Root.ChangeEventDetails, Tabs.Root.ChangeEventReason, Tabs.Root.Orientation, Tabs.Root.Props, Tabs.Root.State, Tabs.Tab.ActivationDirection, Tabs.Tab.Metadata, Tabs.Tab.Position, Tabs.Tab.Props, Tabs.Tab.Size, Tabs.Tab.State, Tabs.Tab.Value
- Tabs - LinkTab
- Props: className, disabled, render, style, value
- Data Attributes: data-activation-direction, data-active, data-disabled, data-orientation
- Types: Tabs.Indicator.Props, Tabs.Indicator.State, Tabs.LinkTab.Props, Tabs.LinkTab.State, Tabs.List.Props, Tabs.List.State, Tabs.Panel.Metadata, Tabs.Panel.Props, Tabs.Panel.State, Tabs.Root.ChangeEventDetails, Tabs.Root.ChangeEventReason, Tabs.Root.Orientation, Tabs.Root.Props, Tabs.Root.State, Tabs.Tab.ActivationDirection, Tabs.Tab.Metadata, Tabs.Tab.Position, Tabs.Tab.Props, Tabs.Tab.Size, Tabs.Tab.State, Tabs.Tab.Value

</details>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
.Tabs {
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
}

.Route {
display: flex;
align-items: center;
gap: 0.5rem;
border-bottom: 1px solid var(--color-gray-200);
padding: 0.5rem 0.75rem;
color: var(--color-gray-500);
font-size: 0.8125rem;
line-height: 1rem;
}

.RouteLabel {
white-space: nowrap;
}

.RouteValue {
border-radius: 0.25rem;
background-color: var(--color-gray-100);
color: var(--color-gray-900);
font-family: var(--font-mono);
padding: 0.125rem 0.25rem;
}

.List {
display: flex;
position: relative;
z-index: 0;
padding-inline: 0.25rem;
gap: 0.25rem;
box-shadow: inset 0 -1px var(--color-gray-200);
}

.LinkTab {
display: flex;
align-items: center;
justify-content: center;
outline: 0;
color: var(--color-gray-600);
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
text-decoration: none;
user-select: none;
white-space: nowrap;
word-break: keep-all;
padding-inline: 0.5rem;
padding-block: 0;
height: 2rem;

&[data-active] {
color: var(--color-gray-900);
}

@media (hover: hover) {
&:hover {
color: var(--color-gray-900);
}
}

&:focus-visible {
position: relative;

&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
}

.Indicator {
position: absolute;
z-index: -1;
left: 0;
top: 50%;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 1.5rem;
border-radius: 0.25rem;
background-color: var(--color-gray-100);
transition-property: translate, width;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}

.Panel {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
min-height: 8rem;
outline: 0;
padding: 1.5rem;

&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
border-radius: 0.375rem;
}

&[hidden] {
display: none;
}
}

.PanelText {
margin: 0;
max-width: 20rem;
color: var(--color-gray-700);
font-size: 0.9375rem;
line-height: 1.5rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client';
import * as React from 'react';
import { Link, MemoryRouter, useLocation } from 'react-router';
import { Tabs } from '@base-ui/react/tabs';
import styles from './index.module.css';

const routes = [
{
path: '/overview',
label: 'Overview',
description: 'Review the latest activity and key project updates.',
},
{
path: '/projects',
label: 'Projects',
description: 'Track milestones, assignments, and project health.',
},
{
path: '/account',
label: 'Account',
description: 'Manage profile details, permissions, and preferences.',
},
] as const;

export default function ExampleTabsLinks() {
return (
<MemoryRouter initialEntries={[routes[0].path]}>
<RouterTabs />
</MemoryRouter>
);
}

function RouterTabs() {
const location = useLocation();
const activeRoute = routes.find((route) => route.path === location.pathname) ?? routes[0];

return (
<Tabs.Root className={styles.Tabs} value={activeRoute.path}>
<div className={styles.Route}>
<span className={styles.RouteLabel}>Current route</span>
<code className={styles.RouteValue}>{location.pathname}</code>
</div>
<Tabs.List className={styles.List}>
{routes.map((route) => (
<Tabs.LinkTab
key={route.path}
className={styles.LinkTab}
render={<Link to={route.path} />}
value={route.path}
>
{route.label}
</Tabs.LinkTab>
))}
<Tabs.Indicator className={styles.Indicator} />
</Tabs.List>
{routes.map((route) => (
<Tabs.Panel key={route.path} className={styles.Panel} value={route.path}>
<p className={styles.PanelText}>{route.description}</p>
</Tabs.Panel>
))}
</Tabs.Root>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createDemoWithVariants } from 'docs/src/utils/createDemo';
import CssModules from './css-modules';
import Tailwind from './tailwind';

export const DemoTabsLinks = createDemoWithVariants(import.meta.url, { CssModules, Tailwind });
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';
import * as React from 'react';
import { Link, MemoryRouter, useLocation } from 'react-router';
import { Tabs } from '@base-ui/react/tabs';

const routes = [
{
path: '/overview',
label: 'Overview',
description: 'Review the latest activity and key project updates.',
},
{
path: '/projects',
label: 'Projects',
description: 'Track milestones, assignments, and project health.',
},
{
path: '/account',
label: 'Account',
description: 'Manage profile details, permissions, and preferences.',
},
] as const;

const linkTabClassName = `
flex h-8 items-center justify-center px-2
text-sm font-normal break-keep whitespace-nowrap text-gray-600 no-underline
outline-hidden select-none
before:inset-x-0 before:inset-y-1 before:rounded-xs
before:-outline-offset-1 before:outline-blue-800
hover:text-gray-900 data-[active]:text-gray-900
focus-visible:relative focus-visible:before:absolute
focus-visible:before:outline focus-visible:before:outline-2
`;

const indicatorClassName = `
absolute top-1/2 left-0 z-[-1]
h-6 w-[var(--active-tab-width)] rounded-xs bg-gray-100
translate-x-[var(--active-tab-left)] -translate-y-1/2
transition-all duration-200 ease-in-out
`;

const panelClassName = `
relative flex min-h-32 items-center p-6
-outline-offset-1 outline-blue-800
focus-visible:rounded-md focus-visible:outline-2
`;

export default function ExampleTabsLinks() {
return (
<MemoryRouter initialEntries={[routes[0].path]}>
<RouterTabs />
</MemoryRouter>
);
}

function RouterTabs() {
const location = useLocation();
const activeRoute = routes.find((route) => route.path === location.pathname) ?? routes[0];

return (
<Tabs.Root className="rounded-md border border-gray-200" value={activeRoute.path}>
<div className="flex items-center gap-2 border-b border-gray-200 px-3 py-2 text-xs text-gray-500">
<span>Current route</span>
<code className="rounded-sm bg-gray-100 px-1 py-0.5 font-mono text-xs leading-4 text-gray-900">
{location.pathname}
</code>
</div>
<Tabs.List className="relative z-0 flex gap-1 px-1 shadow-[inset_0_-1px] shadow-gray-200">
{routes.map((route) => (
<Tabs.LinkTab
key={route.path}
className={linkTabClassName}
render={<Link to={route.path} />}
value={route.path}
>
{route.label}
</Tabs.LinkTab>
))}
<Tabs.Indicator className={indicatorClassName} />
</Tabs.List>
{routes.map((route) => (
<Tabs.Panel key={route.path} className={panelClassName} value={route.path}>
<p className="m-0 max-w-80 text-[0.9375rem] leading-6 text-gray-700">
{route.description}
</p>
</Tabs.Panel>
))}
</Tabs.Root>
);
}
43 changes: 29 additions & 14 deletions docs/src/app/(docs)/react/components/tabs/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/>

import { DemoTabsHero } from './demos/hero';
import { DemoTabsLinks } from './demos/links';

<DemoTabsHero />

Expand All @@ -31,23 +32,33 @@ import { Tabs } from '@base-ui/react/tabs';

### Links

Use the `render` prop and set `nativeButton={false}` on `<Tabs.Tab>` to render tabs as anchor elements.
Use the `<Tabs.LinkTab>` part to render tabs as anchor elements.

```jsx title="Tabs as links"
<DemoTabsLinks />

When rendering with a router link component (such as React Router or Next.js `Link`), control the `value` prop on `<Tabs.Root>` from the current route.
Router links intercept clicks to navigate client-side, so the URL should remain the source of truth.
Deriving `value` from the URL keeps the indicator in sync with clicks, browser back/forward, redirects, and route changes that happen outside the tab list.

```jsx title="Router-driven tabs"
import { Link, useLocation } from 'react-router';
import { Tabs } from '@base-ui/react/tabs';
import Link from 'next/link';

<Tabs.Root>
<Tabs.List>
{/* @highlight-start */}
{/* @highlight-text "nativeButton={false}" "render" */}
<Tabs.Tab nativeButton={false} render={<Link href="/overview" />} value="overview">
Overview
</Tabs.Tab>
{/* @highlight-end */}
</Tabs.List>
{/* ... */}
</Tabs.Root>;
function NavTabs() {
const location = useLocation();
return (
<Tabs.Root value={location.pathname}>
<Tabs.List>
<Tabs.LinkTab value="/overview" render={<Link to="/overview" />}>
Overview
</Tabs.LinkTab>
<Tabs.LinkTab value="/projects" render={<Link to="/projects" />}>
Projects
</Tabs.LinkTab>
</Tabs.List>
</Tabs.Root>
);
}
```

## API reference
Expand All @@ -66,6 +77,10 @@ import { TypesTabs } from './types';

<TypesTabs.Tab />

### LinkTab

<TypesTabs.LinkTab />

### Indicator

<TypesTabs.Indicator />
Expand Down
Loading
Loading