Skip to content

feat: add Tabs & subcomponents - #3289

Merged
colepeters merged 16 commits into
v5-alphafrom
sdk-2030/tab
Oct 29, 2026
Merged

colepeters merged 16 commits into
v5-alphafrom
sdk-2030/tab

Conversation

@colepeters

@colepeters colepeters commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Description

This PR introduces the new Tabs component and its subcomponents (Tabs.List, Tabs.Tab, Tabs.Panel). This is a complete rewrite of the existing Tabs components and is a breaking change — therefore, there are no codemods included with this work.

The root Tabs component sets up a React Context provider; this provider takes value and onValueChange props from the consumer. value sets the active tab, while onValueChange is an updater function for the value. Typically, these will be provided via useState variables and setters, but the consumer is free to use any methodology they wish.

The Tabs.List component is mainly responsible for establishing the correct accessibility tree and the layout for the enclosed tabs. Tabs.List requires either aria-label or aria-labelledby from the consumer to correctly label the tab list.

Tabs.Tab takes the following:

  • value: a string representing the ID of the tab, which will be repeated for its matching Tab.Panel; this is the value to use in the root Tabs component for denoting the active tab
  • label: a string to use as the Tab’s text label
  • icon: an optional icon
  • tone: an optional Tone

Finally, Tab.Panel takes a value which is the same value string as its matching Tabs.Tab.

When a tab is clicked or activated via keyboard (see below), the provided onValueChange function will fire with that tab’s value as the argument for the function.

As a whole, Tabs and its subcomponents align with the recommended WAI-ARIA Tabs example (manual activation branch). As such, Tabs.List has an onKeyDown listener, which handles the keyboard interaction recommendations in the WAI-ARIA example:

  • Tab key moves focus into Tabs.List, to the currently active tab
  • left and right arrows cycle through the available tabs (looping at start and end)
  • space/enter activates the tab
  • Tab key moves focus to the active Tab.Panel

On the last point above: the active Tab.Panel has a default tabindex of 0 to enable focusing when there is no focusable content. Tab panels with focusable content should unset the panel’s tabindex so that the panel’s first focusable component receives focus instead. This is an end user concern, so we should be sure to document this.

What to review

  • Do the components look and work as expected?
  • Is the code understandable? Any improvements/changes recommended?
  • Do tests cover everything sufficiently?

Testing

All functionality should be tested (some of it in a Storybook story which is excluded from the Storybook UI but is run via Vitest).


Note

Medium Risk
Introduces a breaking rewrite of Tabs as a new public, accessibility-sensitive API; keyboard and ARIA behavior need correct consumer usage (e.g. panel tabindex when content is focusable).

Overview
Adds a new Tabs API to @sanity/ui (Tabs, Tabs.List, Tabs.Tab, Tabs.Panel) with controlled value / onValueChange, styling in tabs.css, and exports wired through the package index and changeset (patch).

Tabs.List requires aria-label or aria-labelledby, implements WAI-ARIA tablist behavior (arrow/Home/End focus on tabs; selection via click), and panels show/hide with display plus default panel tabIndex={0} for keyboard exit from the tab strip. Tabs support optional icons and tones.

Storybook adds Tabs stories (default, tones/icons, Vitest-only interaction coverage). depth-testing, inp-testing, and mount-testing routes gain tab demos; ui3/ui4 still use separate Tab / TabList / TabPanel imports while ui5 uses the new compound Tabs.

Also turns off react/jsx-no-constructed-context-values in oxlint (React Compiler), updates create-ui.js Storybook a11y context to [data-ui="…"], and bumps the generated VERSION.

Reviewed by Cursor Bugbot for commit 6906416. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated
sanity-ui-storybook Ignored Ignored Oct 29, 2026 10:28pm UTC
sanity-ui-workshop Ignored Ignored Oct 29, 2026 10:28pm UTC
ui-docs Ignored Ignored Oct 29, 2026 10:28pm UTC
ui-studio Ignored Ignored Oct 29, 2026 10:28pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6906416

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sanity/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ec762d0. Configure here.

@laurenashpole laurenashpole left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! All my comments include the word "could" because they're nitpicky personal preference things you can do or not if you want.

Comment thread bin/create-ui.js
parameters: {
a11y: {
context: '.sui-${componentName}',
context: '[data-ui="${componentName}"]',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't believe I missed this!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot going on in this repo, things get easy to miss ;)

return context
}

function getTabId(tabsRootId: string, value: string): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could combine these into a getId that also accepts a suffix.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like having two simple functions instead of one function with two control flows personally, and also I can't seem to come up with a sensible name for the merged function. But it's a loose opinion so if this happens to change in the future I won’t object ;)

@colepeters
colepeters merged commit 933796b into v5-alpha Oct 29, 2026
15 of 16 checks passed
@colepeters
colepeters deleted the sdk-2030/tab branch October 29, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants