Skip to content

Commit e6f77e7

Browse files
authored
Tabs redesign (#3274)
* remove index files * change tab styles * add glider and scroll indicators * add small padding to tabs * update tests * fix scroll indicator color * fix scroll indicator color * Mark component as stable * add mobile screenshots * add internal components to storybook * add description for the stretched prop * update documentation * lint * fix stretched tabs overflow styles * update styles on resize * fix tablist border width * replace scrollIntoView with scrollLeft * default initialSelectedIndex to 0 if given value is invalid * make glider animation a progressive enhancement * improve initialisation of selected id * improve glider styles
1 parent f1c1e6d commit e6f77e7

File tree

16 files changed

+444
-141
lines changed

16 files changed

+444
-141
lines changed

.changeset/hip-phones-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sumup-oss/circuit-ui": minor
3+
---
4+
5+
Updated the styles of the Tabs component to align with the updated design system guidelines.
Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,73 @@
1-
import { Meta, Status, Props, Story } from '../../../../.storybook/components';
1+
import { Meta, Status, Props, Story } from "../../../../.storybook/components";
22

3-
import * as Stories from './Tabs.stories';
3+
import * as Stories from "./Tabs.stories";
44

55
<Meta of={Stories} />
66

77
# Tabs
88

9-
<Status variant="under-review">
10-
Does not handle content overflow on mobile. Unclear use case compared to
11-
subnavigation.
12-
</Status>
9+
<Status variant="stable" />
10+
11+
The Tabs component allows users to navigate between different views or sections of content within the same context.
1312

1413
<Story of={Stories.Base} />
1514

1615
<Props />
1716

18-
## Variations
17+
## When to use it
18+
19+
### Group related content
20+
21+
Tabs help optimize space by organizing large amounts of content into easily scannable sections, reducing cognitive load for users. They provide simple navigation between related sections that don’t require side-by-side comparison.
22+
23+
Examples include:
24+
25+
- Switching between subsets of the same data: Active Orders | Completed Orders | Cancelled Orders
26+
27+
- Breaking a long process into steps: Personal Info | Delivery Details | Payment Details | Review
28+
29+
### Navigation
30+
31+
Use tabs to navigate between a set of related pages within a larger website or application. For example, in a user profile section:
32+
33+
Overview | Posts | Comments | Settings
1934

20-
If you need more control on the tabs, you can build you own tabs list instead of passing an array of items to the `Tabs` component. This can be useful to style any of the components individually.
35+
## How to use it
36+
37+
### Use the Tabs component directly
38+
39+
You can use the Tabs component out of the box to create a tabbed view by providing an array of tab items. Each tab should include:
40+
41+
- a unique id
42+
- a clear, concise label
43+
- the content to display when the tab is active
44+
45+
### Use subcomponents independently
46+
47+
If you need finer control over your layout, you can use the `Tab`, `TabList`, and `TabPanel` components independently. This gives you flexibility to customize the structure and behavior of your tabs while still relying on the core functionality provided by Tabs.
48+
49+
⚠️ When doing so, you are responsible for ensuring accessibility and managing state correctly.
50+
51+
- Using Tab:
52+
- Provide a unique `id` for each tab.
53+
- Add an `aria-controls` attribute that points to the id of its corresponding panel.
54+
- Implement keyboard navigation (e.g., arrow keys, Home/End) to support keyboard users.
55+
- Using TabPanel:
56+
- Assign a unique id to each panel.
57+
- Ensure this id matches the `aria-controls` value of its corresponding tab.
58+
- Set `aria-labelledby` to reference the id of the associated tab.
2159

2260
<Story of={Stories.ControlledState} />
2361

24-
## With links
62+
### With links
63+
64+
Tabs render as buttons by default. In navigation contexts, you can pass a `href` prop to render them as links.
65+
Avoid using external links, as this can create an inconsistent user experience across tabs.
2566

2667
<Story of={Stories.Links} />
2768

28-
## When to use it
69+
### Stretched
70+
71+
Set the `stretched` prop to true to make all tabs expand and share the available space evenly.
2972

30-
## Usage guidelines
73+
<Story of={Stories.Stretched} />

packages/circuit-ui/components/Tabs/Tabs.spec.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@
1313
* limitations under the License.
1414
*/
1515

16-
import { describe, expect, it } from 'vitest';
16+
import { beforeAll, describe, expect, it, vi } from 'vitest';
1717

1818
import { axe, render, screen, userEvent } from '../../util/test-utils.js';
1919

20-
import { TabPanel } from './components/TabPanel/index.js';
21-
import { TabList } from './components/TabList/index.js';
22-
import { Tab } from './components/Tab/index.js';
20+
import { TabPanel } from './components/TabPanel/TabPanel.js';
21+
import { TabList } from './components/TabList/TabList.js';
22+
import { Tab } from './components/Tab/Tab.js';
2323
import { Tabs } from './Tabs.js';
2424

2525
describe('Tabs', () => {
26+
beforeAll(() => {
27+
HTMLElement.prototype.scrollIntoView = vi.fn();
28+
});
2629
it('should switch panels on tab click', async () => {
2730
render(
2831
<Tabs

packages/circuit-ui/components/Tabs/Tabs.stories.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ import { ArrowLeft, ExternalLink } from '@sumup-oss/icons';
1919
import { Body } from '../Body/index.js';
2020
import { Headline } from '../Headline/index.js';
2121
import { Button } from '../Button/index.js';
22+
import { modes } from '../../../../.storybook/modes.js';
2223

2324
import type { TabsProps } from './Tabs.js';
24-
25-
import { Tabs, TabList, TabPanel, Tab } from './index.js';
25+
import { Tabs } from './Tabs.js';
26+
import { TabList } from './components/TabList/TabList.js';
27+
import { TabPanel } from './components/TabPanel/TabPanel.js';
28+
import { Tab } from './components/Tab/Tab.js';
2629

2730
export default {
2831
title: 'Navigation/Tabs',
@@ -31,6 +34,12 @@ export default {
3134
tags: ['status:under-review'],
3235
parameters: {
3336
layout: 'fullscreen',
37+
chromatic: {
38+
modes: {
39+
mobile: modes.smallMobile,
40+
desktop: modes.desktop,
41+
},
42+
},
3443
},
3544
};
3645

@@ -85,6 +94,16 @@ const tabs = [
8594
tab: 'Tab 4',
8695
panel: <ContentWithInteractiveElements index={4} />,
8796
},
97+
{
98+
id: 'five',
99+
tab: 'Tab 5',
100+
panel: <ContentWithInteractiveElements index={5} />,
101+
},
102+
{
103+
id: 'six',
104+
tab: 'Tab 6',
105+
panel: <ContentWithInteractiveElements index={6} />,
106+
},
88107
];
89108

90109
export const Base = (args: TabsProps) => <Tabs {...args} />;
@@ -94,18 +113,20 @@ Base.args = {
94113
stretched: false,
95114
};
96115

116+
export const Stretched = (args: TabsProps) => <Tabs {...args} />;
117+
118+
Stretched.args = {
119+
items: tabs,
120+
stretched: true,
121+
};
122+
97123
export const Links = () => (
98124
<TabList>
99125
<Tab selected>Home</Tab>
100-
<Tab href="https://github.com/sumup-oss/circuit-ui" target="_blank">
101-
GitHub
102-
</Tab>
103-
<Tab
104-
href="https://www.npmjs.com/package/@sumup-oss/circuit-ui"
105-
target="_blank"
106-
>
107-
NPM
126+
<Tab href="#posts" target="">
127+
Posts
108128
</Tab>
129+
<Tab href="#reviews">Reviews</Tab>
109130
</TabList>
110131
);
111132

packages/circuit-ui/components/Tabs/Tabs.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import {
2323
isArrowDown,
2424
} from '../../util/key-codes.js';
2525

26-
import { TabList, type TabListProps } from './components/TabList/index.js';
27-
import { Tab } from './components/Tab/index.js';
28-
import { TabPanel } from './components/TabPanel/index.js';
26+
import { TabList, type TabListProps } from './components/TabList/TabList.js';
27+
import { Tab } from './components/Tab/Tab.js';
28+
import { TabPanel } from './components/TabPanel/TabPanel.js';
2929

3030
export interface TabsProps extends TabListProps {
3131
/**
@@ -45,7 +45,9 @@ export interface TabsProps extends TabListProps {
4545
}
4646

4747
export function Tabs({ items, initialSelectedIndex = 0, ...props }: TabsProps) {
48-
const [selectedId, setSelectedId] = useState(items[initialSelectedIndex]?.id);
48+
const [selectedId, setSelectedId] = useState(
49+
items[initialSelectedIndex]?.id ?? items[0]?.id,
50+
);
4951

5052
const handleTabKeyDown = (event: KeyboardEvent) => {
5153
const selectedIndex = items.findIndex((item) => item.id === selectedId);

packages/circuit-ui/components/Tabs/components/Tab/Tab.module.css

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
.base {
2+
--selected-tab-pseudo-content: "";
3+
24
float: left;
35
display: flex;
46
flex-direction: column;
57
align-items: center;
68
justify-content: center;
7-
height: 100%;
8-
padding: var(--cui-spacings-kilo) var(--cui-spacings-tera);
9-
font-size: var(--cui-body-m-font-size);
10-
line-height: var(--cui-body-m-line-height);
9+
padding: 0 var(--cui-spacings-bit);
10+
margin: var(--cui-spacings-kilo) 0;
11+
font-size: var(--cui-body-s-font-size);
12+
font-weight: var(--cui-font-weight-semibold);
13+
line-height: var(--cui-body-s-line-height);
1114
color: var(--cui-fg-subtle);
1215
white-space: nowrap;
1316
text-decoration: none;
1417
cursor: pointer;
1518
outline: none;
1619
background-color: var(--cui-bg-normal);
1720
border: none;
21+
border-radius: var(--cui-border-radius-bit);
22+
transition: color var(--cui-transitions-default);
1823
}
1924

2025
.base:hover {
21-
background-color: var(--cui-bg-normal-hovered);
26+
color: var(--cui-fg-normal-hovered);
2227
}
2328

2429
.base:focus {
@@ -35,7 +40,7 @@
3540
}
3641

3742
.base:active {
38-
background-color: var(--cui-bg-normal-pressed);
43+
color: var(--cui-fg-normal);
3944
}
4045

4146
.base[aria-selected="true"] {
@@ -47,11 +52,15 @@
4752
.base[aria-selected="true"]::after {
4853
position: absolute;
4954
right: 0;
50-
bottom: 0;
55+
bottom: calc(
56+
-1 * calc(var(--cui-spacings-kilo) - var(--cui-border-width-kilo))
57+
);
5158
left: 0;
5259
z-index: var(--cui-z-index-absolute);
53-
width: 100%;
54-
height: var(--cui-spacings-bit);
55-
content: " ";
56-
background: var(--cui-border-accent);
60+
height: 3px;
61+
content: var(--selected-tab-pseudo-content);
62+
background-color: var(--cui-border-accent);
63+
border-top-left-radius: var(--cui-border-radius-byte);
64+
border-top-right-radius: var(--cui-border-radius-byte);
65+
transition: all var(--cui-transitions-default);
5766
}

packages/circuit-ui/components/Tabs/components/Tab/index.ts renamed to packages/circuit-ui/components/Tabs/components/Tab/Tab.stories.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2019, SumUp Ltd.
2+
* Copyright 2025, SumUp Ltd.
33
* Licensed under the Apache License, Version 2.0 (the "License");
44
* you may not use this file except in compliance with the License.
55
* You may obtain a copy of the License at
@@ -13,6 +13,23 @@
1313
* limitations under the License.
1414
*/
1515

16-
export { Tab } from './Tab.js';
16+
import { Tab } from './Tab.js';
1717

18-
export type { TabProps } from './Tab.js';
18+
export default {
19+
title: 'Navigation/Tabs/Tab',
20+
component: Tab,
21+
tags: ['status:internal'],
22+
parameters: {
23+
chromatic: {
24+
disableSnapshot: true,
25+
},
26+
},
27+
};
28+
29+
export const Base = () => (
30+
<div>
31+
<Tab>Button</Tab>
32+
<Tab href="#link">Link</Tab>
33+
<Tab selected>Selected</Tab>
34+
</div>
35+
);

packages/circuit-ui/components/Tabs/components/Tab/Tab.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,16 @@ import { clsx } from '../../../../styles/clsx.js';
2727

2828
import classes from './Tab.module.css';
2929

30-
interface BaseProps {
31-
/**
32-
* Triggers selected styles of the component
33-
*/
34-
selected?: boolean;
35-
}
36-
3730
type LinkElProps = AnchorHTMLAttributes<HTMLAnchorElement>;
3831
type ButtonElProps = ButtonHTMLAttributes<HTMLButtonElement>;
3932

40-
export type TabProps = BaseProps & LinkElProps & ButtonElProps;
33+
export type TabProps = LinkElProps &
34+
ButtonElProps & {
35+
/**
36+
* Triggers selected styles of the component
37+
*/
38+
selected?: boolean;
39+
};
4140

4241
const tabIndex = (selected: boolean) => (selected ? undefined : -1);
4342

0 commit comments

Comments
 (0)