diff --git a/apps/vr-tests-react-components/src/stories/Tabs.stories.tsx b/apps/vr-tests-react-components/src/stories/Tabs.stories.tsx
index 6924e258e895c9..409afb8012da87 100644
--- a/apps/vr-tests-react-components/src/stories/Tabs.stories.tsx
+++ b/apps/vr-tests-react-components/src/stories/Tabs.stories.tsx
@@ -206,3 +206,31 @@ export const WithIconOnlyAndVertical = () => (
);
WithIconOnlyAndVertical.storyName = 'With icon only and vertical';
+
+export const SubtleCircularAppearance = () => (
+
+ First
+
+ Second
+
+ Third
+
+);
+
+export const SubtleCircularAppearanceDarkMode = getStoryVariant(SubtleCircularAppearance, DARK_MODE);
+
+export const SubtleCircularAppearanceHighContrast = getStoryVariant(SubtleCircularAppearance, HIGH_CONTRAST);
+
+export const FilledCircularAppearance = () => (
+
+ First
+
+ Second
+
+ Third
+
+);
+
+export const FilledCircularAppearanceDarkMode = getStoryVariant(FilledCircularAppearance, DARK_MODE);
+
+export const FilledCircularAppearanceHighContrast = getStoryVariant(FilledCircularAppearance, HIGH_CONTRAST);
diff --git a/change/@fluentui-react-tabs-3bd10027-20f4-490c-9384-f87d93d8fe3a.json b/change/@fluentui-react-tabs-3bd10027-20f4-490c-9384-f87d93d8fe3a.json
new file mode 100644
index 00000000000000..376fa2cd8307c1
--- /dev/null
+++ b/change/@fluentui-react-tabs-3bd10027-20f4-490c-9384-f87d93d8fe3a.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feature: add rounded tab variant",
+ "packageName": "@fluentui/react-tabs",
+ "email": "kirpadv@gmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-tabs/library/etc/react-tabs.api.md b/packages/react-components/react-tabs/library/etc/react-tabs.api.md
index cefd7fea77e99e..ac9c5249cf0af4 100644
--- a/packages/react-components/react-tabs/library/etc/react-tabs.api.md
+++ b/packages/react-components/react-tabs/library/etc/react-tabs.api.md
@@ -66,7 +66,7 @@ export type TabListContextValues = {
// @public
export type TabListProps = ComponentProps & {
- appearance?: 'transparent' | 'subtle';
+ appearance?: 'transparent' | 'subtle' | 'subtle-circular' | 'filled-circular';
reserveSelectedTabSpace?: boolean;
defaultSelectedValue?: TabValue;
disabled?: boolean;
@@ -109,7 +109,7 @@ export type TabSlots = {
// @public
export type TabState = ComponentState & Pick & Required> & {
- appearance?: 'transparent' | 'subtle';
+ appearance?: 'transparent' | 'subtle' | 'subtle-circular' | 'filled-circular';
iconOnly: boolean;
selected: boolean;
contentReservedSpaceClassName?: string;
diff --git a/packages/react-components/react-tabs/library/src/components/Tab/Tab.types.ts b/packages/react-components/react-tabs/library/src/components/Tab/Tab.types.ts
index a939cf2435f9b4..a565d5576da60c 100644
--- a/packages/react-components/react-tabs/library/src/components/Tab/Tab.types.ts
+++ b/packages/react-components/react-tabs/library/src/components/Tab/Tab.types.ts
@@ -50,9 +50,10 @@ export type TabState = ComponentState &
Pick &
Required> & {
/**
- * A tab supports 'transparent' and 'subtle' appearance.
+ * A tab supports 'transparent', 'subtle', `subtle-circular` and `filled-circular` appearance.
*/
- appearance?: 'transparent' | 'subtle';
+ appearance?: 'transparent' | 'subtle' | 'subtle-circular' | 'filled-circular';
+
/**
* A tab can have only an icon slot filled and no content.
*/
diff --git a/packages/react-components/react-tabs/library/src/components/Tab/useTabStyles.styles.ts b/packages/react-components/react-tabs/library/src/components/Tab/useTabStyles.styles.ts
index e65c7d92baddd0..ecd0084fbed5ce 100644
--- a/packages/react-components/react-tabs/library/src/components/Tab/useTabStyles.styles.ts
+++ b/packages/react-components/react-tabs/library/src/components/Tab/useTabStyles.styles.ts
@@ -88,89 +88,168 @@ const useRootStyles = makeStyles({
},
transparent: {
backgroundColor: tokens.colorTransparentBackground,
- ':hover': {
+ ':enabled:hover': {
backgroundColor: tokens.colorTransparentBackgroundHover,
},
- ':active': {
+ ':enabled:active': {
backgroundColor: tokens.colorTransparentBackgroundPressed,
},
- '& .fui-Tab__icon': {
+ [`& .${tabClassNames.icon}`]: {
color: tokens.colorNeutralForeground2,
},
- ':hover .fui-Tab__icon': {
+ [`:enabled:hover .${tabClassNames.icon}`]: {
color: tokens.colorNeutralForeground2Hover,
},
- ':active .fui-Tab__icon': {
+ [`:enabled:active .${tabClassNames.icon}`]: {
color: tokens.colorNeutralForeground2Pressed,
},
- '& .fui-Tab__content': {
+ [`& .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground2,
},
- ':hover .fui-Tab__content': {
+ [`:enabled:hover .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground2Hover,
},
- ':active .fui-Tab__content': {
+ [`:enabled:active .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground2Pressed,
},
},
subtle: {
backgroundColor: tokens.colorSubtleBackground,
- ':hover': {
+ ':enabled:hover': {
backgroundColor: tokens.colorSubtleBackgroundHover,
},
- ':active': {
+ ':enabled:active': {
backgroundColor: tokens.colorSubtleBackgroundPressed,
},
- '& .fui-Tab__icon': {
+ [`& .${tabClassNames.icon}`]: {
color: tokens.colorNeutralForeground2,
},
- ':hover .fui-Tab__icon': {
+ [`:enabled:hover .${tabClassNames.icon}`]: {
color: tokens.colorNeutralForeground2Hover,
},
- ':active .fui-Tab__icon': {
+ [`:enabled:active .${tabClassNames.icon}`]: {
color: tokens.colorNeutralForeground2Pressed,
},
- '& .fui-Tab__content': {
+ [`& .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground2,
},
- ':hover .fui-Tab__content': {
+ [`:enabled:hover .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground2Hover,
},
- ':active .fui-Tab__content': {
+ [`:enabled:active .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground2Pressed,
},
},
disabled: {
backgroundColor: tokens.colorTransparentBackground,
- '& .fui-Tab__icon': {
+ [`& .${tabClassNames.icon}`]: {
color: tokens.colorNeutralForegroundDisabled,
},
- '& .fui-Tab__content': {
+ [`& .${tabClassNames.content}`]: {
color: tokens.colorNeutralForegroundDisabled,
},
cursor: 'not-allowed',
},
selected: {
- '& .fui-Tab__icon': {
+ [`& .${tabClassNames.icon}`]: {
color: tokens.colorCompoundBrandForeground1,
},
- ':hover .fui-Tab__icon': {
+ [`:enabled:hover .${tabClassNames.icon}`]: {
color: tokens.colorCompoundBrandForeground1Hover,
},
- ':active .fui-Tab__icon': {
+ [`:enabled:active .${tabClassNames.icon}`]: {
color: tokens.colorCompoundBrandForeground1Pressed,
},
- '& .fui-Tab__content': {
+ [`& .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground1,
},
- ':hover .fui-Tab__content': {
+ [`:enabled:hover .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground1Hover,
},
- ':active .fui-Tab__content': {
+ [`:enabled:active .${tabClassNames.content}`]: {
color: tokens.colorNeutralForeground1Pressed,
},
},
+ circular: {
+ borderRadius: tokens.borderRadiusCircular,
+ [`& .${tabClassNames.icon}`]: {
+ color: 'inherit',
+ },
+ [`& .${tabClassNames.content}`]: {
+ color: 'inherit',
+ },
+ },
+ subtleCircular: {
+ backgroundColor: tokens.colorTransparentBackground,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorTransparentStroke}`,
+ color: tokens.colorNeutralForeground2,
+ ':enabled:hover': {
+ backgroundColor: tokens.colorNeutralBackground1Hover,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorNeutralStroke1Hover}`,
+ color: tokens.colorNeutralForeground2Hover,
+ },
+ ':enabled:active': {
+ backgroundColor: tokens.colorNeutralBackground1Pressed,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorNeutralStroke1Pressed}`,
+ color: tokens.colorNeutralForeground2Pressed,
+ },
+ },
+ subtleCircularSelected: {
+ backgroundColor: tokens.colorBrandBackground2,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorCompoundBrandStroke}`,
+ color: tokens.colorBrandForeground2,
+ ':enabled:hover': {
+ backgroundColor: tokens.colorBrandBackground2Hover,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorCompoundBrandStrokeHover}`,
+ color: tokens.colorBrandForeground2Hover,
+ },
+ ':enabled:active': {
+ backgroundColor: tokens.colorBrandBackground2Pressed,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorCompoundBrandStrokePressed}`,
+ color: tokens.colorBrandForeground2Pressed,
+ },
+ },
+ subtleCircularDisabled: {
+ backgroundColor: tokens.colorTransparentBackground,
+ color: tokens.colorNeutralForegroundDisabled,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorTransparentStroke}`,
+ },
+ subtleCircularDisabledSelected: {
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorNeutralStrokeDisabled}`,
+ },
+ filledCircular: {
+ backgroundColor: tokens.colorNeutralBackground3,
+ color: tokens.colorNeutralForeground2,
+ ':enabled:hover': {
+ backgroundColor: tokens.colorNeutralBackground3Hover,
+ color: tokens.colorNeutralForeground2Hover,
+ },
+ ':enabled:active': {
+ backgroundColor: tokens.colorNeutralBackground3Pressed,
+ color: tokens.colorNeutralForeground2Pressed,
+ },
+ },
+ filledCircularSelected: {
+ backgroundColor: tokens.colorBrandBackground,
+ color: tokens.colorNeutralForegroundOnBrand,
+ ':enabled:hover': {
+ backgroundColor: tokens.colorBrandBackgroundHover,
+ color: tokens.colorNeutralForegroundOnBrand,
+ },
+ ':enabled:active': {
+ backgroundColor: tokens.colorBrandBackgroundPressed,
+ color: tokens.colorNeutralForegroundOnBrand,
+ },
+ },
+ filledCircularDisabled: {
+ backgroundColor: tokens.colorNeutralBackgroundDisabled,
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorTransparentStroke}`,
+ color: tokens.colorNeutralForegroundDisabled,
+ },
+ filledCircularDisabledSelected: {
+ border: `solid ${tokens.strokeWidthThin} ${tokens.colorNeutralStrokeDisabled}`,
+ },
});
/**
@@ -291,20 +370,20 @@ const useActiveIndicatorStyles = makeStyles({
'::after': {
backgroundColor: tokens.colorCompoundBrandStroke,
},
- ':hover::after': {
+ ':enabled:hover::after': {
backgroundColor: tokens.colorCompoundBrandStrokeHover,
},
- ':active::after': {
+ ':enabled:active::after': {
backgroundColor: tokens.colorCompoundBrandStrokePressed,
},
'@media (forced-colors: active)': {
'::after': {
backgroundColor: 'ButtonText',
},
- ':hover::after': {
+ ':enabled:hover::after': {
backgroundColor: 'ButtonText',
},
- ':active::after': {
+ ':enabled:active::after': {
backgroundColor: 'ButtonText',
},
},
@@ -472,35 +551,36 @@ export const useTabIndicatorStyles_unstable = (state: TabState): TabState => {
const pendingIndicatorStyles = usePendingIndicatorStyles();
const activeIndicatorStyles = useActiveIndicatorStyles();
- const { disabled, selected, size, vertical } = state;
-
- state.root.className = mergeClasses(
- tabClassNames.root,
- rootStyles.root,
-
- // pending indicator (before pseudo element)
- pendingIndicatorStyles.base,
- size === 'small' && (vertical ? pendingIndicatorStyles.smallVertical : pendingIndicatorStyles.smallHorizontal),
- size === 'medium' && (vertical ? pendingIndicatorStyles.mediumVertical : pendingIndicatorStyles.mediumHorizontal),
- size === 'large' && (vertical ? pendingIndicatorStyles.largeVertical : pendingIndicatorStyles.largeHorizontal),
- disabled && pendingIndicatorStyles.disabled,
-
- // active indicator (after pseudo element)
- selected && activeIndicatorStyles.base,
- selected && !disabled && activeIndicatorStyles.selected,
- selected &&
- size === 'small' &&
- (vertical ? activeIndicatorStyles.smallVertical : activeIndicatorStyles.smallHorizontal),
- selected &&
- size === 'medium' &&
- (vertical ? activeIndicatorStyles.mediumVertical : activeIndicatorStyles.mediumHorizontal),
- selected &&
- size === 'large' &&
- (vertical ? activeIndicatorStyles.largeVertical : activeIndicatorStyles.largeHorizontal),
- selected && disabled && activeIndicatorStyles.disabled,
-
- state.root.className,
- );
+ const { appearance, disabled, selected, size, vertical } = state;
+
+ const classes: Parameters = [tabClassNames.root, rootStyles.root];
+
+ if (appearance !== 'subtle-circular' && appearance !== 'filled-circular') {
+ classes.push(
+ // pending indicator (before pseudo element)
+ pendingIndicatorStyles.base,
+ size === 'small' && (vertical ? pendingIndicatorStyles.smallVertical : pendingIndicatorStyles.smallHorizontal),
+ size === 'medium' && (vertical ? pendingIndicatorStyles.mediumVertical : pendingIndicatorStyles.mediumHorizontal),
+ size === 'large' && (vertical ? pendingIndicatorStyles.largeVertical : pendingIndicatorStyles.largeHorizontal),
+ disabled && pendingIndicatorStyles.disabled,
+
+ // active indicator (after pseudo element)
+ selected && activeIndicatorStyles.base,
+ selected && !disabled && activeIndicatorStyles.selected,
+ selected &&
+ size === 'small' &&
+ (vertical ? activeIndicatorStyles.smallVertical : activeIndicatorStyles.smallHorizontal),
+ selected &&
+ size === 'medium' &&
+ (vertical ? activeIndicatorStyles.mediumVertical : activeIndicatorStyles.mediumHorizontal),
+ selected &&
+ size === 'large' &&
+ (vertical ? activeIndicatorStyles.largeVertical : activeIndicatorStyles.largeHorizontal),
+ selected && disabled && activeIndicatorStyles.disabled,
+ );
+ }
+
+ state.root.className = mergeClasses(...classes, state.root.className);
useTabAnimatedIndicatorStyles_unstable(state);
@@ -525,16 +605,36 @@ export const useTabButtonStyles_unstable = (state: TabState, slot: TabState['roo
const { appearance, disabled, selected, size, vertical } = state;
+ const isSubtleCircular = appearance === 'subtle-circular';
+ const isFilledCircular = appearance === 'filled-circular';
+ const isCircular = isSubtleCircular || isFilledCircular;
+
+ const circularAppearance = [
+ rootStyles.circular,
+ // subtle-circular appearance
+ isSubtleCircular && rootStyles.subtleCircular,
+ selected && isSubtleCircular && rootStyles.subtleCircularSelected,
+ disabled && isSubtleCircular && rootStyles.subtleCircularDisabled,
+ selected && disabled && isSubtleCircular && rootStyles.subtleCircularDisabledSelected,
+ // filled-circular appearance
+ isFilledCircular && rootStyles.filledCircular,
+ selected && isFilledCircular && rootStyles.filledCircularSelected,
+ disabled && isFilledCircular && rootStyles.filledCircularDisabled,
+ selected && disabled && isFilledCircular && rootStyles.filledCircularDisabledSelected,
+ ];
+
slot.className = mergeClasses(
rootStyles.button,
+ // orientation
vertical ? rootStyles.vertical : rootStyles.horizontal,
+ // size
size === 'small' && (vertical ? rootStyles.smallVertical : rootStyles.smallHorizontal),
size === 'medium' && (vertical ? rootStyles.mediumVertical : rootStyles.mediumHorizontal),
size === 'large' && (vertical ? rootStyles.largeVertical : rootStyles.largeHorizontal),
focusStyles.base,
!disabled && appearance === 'subtle' && rootStyles.subtle,
!disabled && appearance === 'transparent' && rootStyles.transparent,
- !disabled && selected && rootStyles.selected,
+ ...(isCircular ? circularAppearance : [!disabled && selected && rootStyles.selected]),
disabled && rootStyles.disabled,
slot.className,
diff --git a/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts b/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts
index 16f1feaed473cb..79c84b8ce5d58b 100644
--- a/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts
+++ b/packages/react-components/react-tabs/library/src/components/TabList/TabList.types.ts
@@ -42,10 +42,13 @@ export type TabListProps = ComponentProps & {
* A tab list can supports 'transparent' and 'subtle' appearance.
*- 'subtle': Minimizes emphasis to blend into the background until hovered or focused.
*- 'transparent': No background and border styling
+ *- 'subtle-circular': Adds background and border styling
+ *- 'filled-circular': Adds background styling
+ *
* The appearance affects each of the contained tabs.
* @default 'transparent'
*/
- appearance?: 'transparent' | 'subtle';
+ appearance?: 'transparent' | 'subtle' | 'subtle-circular' | 'filled-circular';
/**
* Tab size may change between unselected and selected states.
diff --git a/packages/react-components/react-tabs/library/src/components/TabList/useTabListStyles.styles.ts b/packages/react-components/react-tabs/library/src/components/TabList/useTabListStyles.styles.ts
index 00084869116197..4726160d44b5ab 100644
--- a/packages/react-components/react-tabs/library/src/components/TabList/useTabListStyles.styles.ts
+++ b/packages/react-components/react-tabs/library/src/components/TabList/useTabListStyles.styles.ts
@@ -1,5 +1,6 @@
import { SlotClassNames } from '@fluentui/react-utilities';
import { makeStyles, mergeClasses } from '@griffel/react';
+import { tokens } from '@fluentui/react-theme';
import type { TabListSlots, TabListState } from './TabList.types';
export const tabListClassNames: SlotClassNames = {
@@ -25,6 +26,12 @@ const useStyles = makeStyles({
alignItems: 'stretch',
flexDirection: 'column',
},
+ roundedSmall: {
+ gap: tokens.spacingHorizontalSNudge,
+ },
+ rounded: {
+ gap: tokens.spacingHorizontalS,
+ },
});
/**
@@ -33,14 +40,17 @@ const useStyles = makeStyles({
export const useTabListStyles_unstable = (state: TabListState): TabListState => {
'use no memo';
- const { vertical } = state;
+ const { appearance, vertical, size } = state;
const styles = useStyles();
+ const isRounded = appearance === 'subtle-circular' || appearance === 'filled-circular';
+
state.root.className = mergeClasses(
tabListClassNames.root,
styles.root,
vertical ? styles.vertical : styles.horizontal,
+ isRounded && (size === 'small' ? styles.roundedSmall : styles.rounded),
state.root.className,
);
diff --git a/packages/react-components/react-tabs/stories/src/Tabs/TabListAppearance.stories.tsx b/packages/react-components/react-tabs/stories/src/Tabs/TabListAppearance.stories.tsx
index 1cda26d74ce74c..b6ba4bcd2acd3a 100644
--- a/packages/react-components/react-tabs/stories/src/Tabs/TabListAppearance.stories.tsx
+++ b/packages/react-components/react-tabs/stories/src/Tabs/TabListAppearance.stories.tsx
@@ -19,7 +19,9 @@ export const Appearance = () => {
return (
<>
First Tab
- Second Tab
+
+ Second Tab
+
Third Tab
Fourth Tab
>
@@ -34,6 +36,12 @@ export const Appearance = () => {
{renderTabs()}
+
+ {renderTabs()}
+
+
+ {renderTabs()}
+
);
};
@@ -41,7 +49,8 @@ export const Appearance = () => {
Appearance.parameters = {
docs: {
description: {
- story: 'A tab list can have a `transparent` or `subtle` appearance. The default is `transparent`.',
+ story:
+ 'A tab list can have a `transparent`, `subtle`, `subtle-circular` and `filled-circular` appearance. The default is `transparent`.',
},
},
};