diff --git a/.changeset/famous-panthers-know.md b/.changeset/famous-panthers-know.md
new file mode 100644
index 0000000000..6de1fd9eb7
--- /dev/null
+++ b/.changeset/famous-panthers-know.md
@@ -0,0 +1,5 @@
+---
+"@nextui-org/tabs": patch
+---
+
+Add placement and isVertical prop
diff --git a/apps/docs/content/components/tabs/index.ts b/apps/docs/content/components/tabs/index.ts
index ada7d55342..62c77733c7 100644
--- a/apps/docs/content/components/tabs/index.ts
+++ b/apps/docs/content/components/tabs/index.ts
@@ -10,6 +10,8 @@ import icons from "./icons";
import form from "./form";
import controlled from "./controlled";
import customStyles from "./custom-styles";
+import placement from "./placement";
+import vertical from "./vertical";
export const tabsContent = {
usage,
@@ -24,4 +26,6 @@ export const tabsContent = {
form,
controlled,
customStyles,
+ placement,
+ vertical,
};
diff --git a/apps/docs/content/components/tabs/placement.ts b/apps/docs/content/components/tabs/placement.ts
new file mode 100644
index 0000000000..7f720ac41b
--- /dev/null
+++ b/apps/docs/content/components/tabs/placement.ts
@@ -0,0 +1,54 @@
+const App = `import {Tabs, Tab, Card, CardBody, RadioGroup, Radio} from "@nextui-org/react";
+
+export default function App() {
+ const [placement, setPlacement] = React.useState("top");
+ return (
+
+
setPlacement(value)}
+ >
+ top
+ bottom
+ start
+ end
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
+
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+
+
+
+
+
+
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+
+
+
+ );
+}`;
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/components/tabs/vertical.ts b/apps/docs/content/components/tabs/vertical.ts
new file mode 100644
index 0000000000..7487c22bb1
--- /dev/null
+++ b/apps/docs/content/components/tabs/vertical.ts
@@ -0,0 +1,45 @@
+const App = `import {Tabs, Tab, Card, CardBody, Switch} from "@nextui-org/react";
+
+export default function App() {
+ const [isVertical, setIsVertical] = React.useState(true);
+ return (
+
+
+ Vertical
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+
+
+
+
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+
+
+
+
+
+
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+
+
+
+ );
+}`;
+
+const react = {
+ "/App.jsx": App,
+};
+
+export default {
+ ...react,
+};
diff --git a/apps/docs/content/docs/components/tabs.mdx b/apps/docs/content/docs/components/tabs.mdx
index b927dda7b8..3a1bb195a5 100644
--- a/apps/docs/content/docs/components/tabs.mdx
+++ b/apps/docs/content/docs/components/tabs.mdx
@@ -73,6 +73,18 @@ You can use the `onSelectionChange` and `selectedKey` props to control the selec
+### Placement
+
+You can change the position of the tabs by using the `placement` prop. The default value is `top`.
+
+
+
+### Vertical
+
+Change the orientation of the tabs it will invalidate the placement prop when the value is `true`.
+
+
+
### Links
Tabs items can be rendered as links by passing the `href` prop to the `Tab` component. By
@@ -237,7 +249,9 @@ You can customize the `Tabs` component by passing custom Tailwind CSS classes to
| disableCursorAnimation | `boolean` | Whether the cursor should be hidden. | `false` |
| isDisabled | `boolean` | Whether the tab list should be disabled. | `false` |
| disableAnimation | `boolean` | Whether the tab list should be animated. | `false` |
-| classNames | `Record<"base"| "tabList"| "tab"| "tabContent"| "cursor" | "panel", string>` | Allows to set custom class names for the card slots. | - |
+| classNames | `Record<"base"| "tabList"| "tab"| "tabContent"| "cursor" | "panel", string>` | Allows to set custom class names for the card slots. | - |
+| placement | `top` \| `bottom` \| `start` \| `end` | The position of the tabs. | `top` |
+| isVertical | `boolean` | Whether the tabs are vertical. | `false` |
### Tabs Events
diff --git a/packages/components/tabs/__tests__/tabs.test.tsx b/packages/components/tabs/__tests__/tabs.test.tsx
index e8a7d084f5..a5117c5d6a 100644
--- a/packages/components/tabs/__tests__/tabs.test.tsx
+++ b/packages/components/tabs/__tests__/tabs.test.tsx
@@ -3,7 +3,7 @@ import {act, render} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {focus} from "@nextui-org/test-utils";
-import {Tabs, Tab} from "../src";
+import {Tabs, Tab, TabsProps} from "../src";
type Item = {
id: string;
@@ -29,6 +29,22 @@ let tabs: Item[] = [
},
];
+function getPlacementTemplate(position: TabsProps["placement"]) {
+ return (
+
+
+ Content 1
+
+
+ Content 2
+
+
+ Content 3
+
+
+ );
+}
+
// e.g. console.error Warning: Function components cannot be given refs.
// Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
const spy = jest.spyOn(console, "error").mockImplementation(() => {});
@@ -236,4 +252,70 @@ describe("Tabs", () => {
expect(tab2).toHaveAttribute("aria-selected", "false");
});
+
+ it("should change the position of the tabs", () => {
+ const wrapper = render(getPlacementTemplate("top"));
+
+ const tabWrapper = wrapper.getByTestId("tabWrapper").parentNode;
+
+ expect(tabWrapper).toHaveAttribute("data-placement", "top");
+ expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
+
+ // Test bottom position
+ wrapper.rerender(getPlacementTemplate("bottom"));
+
+ expect(tabWrapper).toHaveAttribute("data-placement", "bottom");
+ expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
+
+ // Test start position
+ wrapper.rerender(getPlacementTemplate("start"));
+
+ expect(tabWrapper).toHaveAttribute("data-placement", "start");
+ expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");
+
+ // Test end position
+ wrapper.rerender(getPlacementTemplate("end"));
+
+ expect(tabWrapper).toHaveAttribute("data-placement", "end");
+ expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");
+ });
+
+ it("should change the orientation of the tabs", () => {
+ const wrapper = render(
+
+
+ Content 1
+
+
+ Content 2
+
+
+ Content 3
+
+ ,
+ );
+
+ const tabWrapper = wrapper.getByTestId("tabWrapper").parentNode;
+
+ expect(tabWrapper).toHaveAttribute("data-placement", "start");
+ expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");
+
+ // Test horizontal orientation
+ wrapper.rerender(
+
+
+ Content 1
+
+
+ Content 2
+
+
+ Content 3
+
+ ,
+ );
+
+ expect(tabWrapper).toHaveAttribute("data-placement", "top");
+ expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
+ });
});
diff --git a/packages/components/tabs/src/tabs.tsx b/packages/components/tabs/src/tabs.tsx
index 59b8651bfe..a4c618ac82 100644
--- a/packages/components/tabs/src/tabs.tsx
+++ b/packages/components/tabs/src/tabs.tsx
@@ -9,7 +9,7 @@ import TabPanel from "./tab-panel";
interface Props extends UseTabsProps {}
function Tabs(props: Props, ref: ForwardedRef) {
- const {Component, values, state, getBaseProps, getTabListProps} = useTabs({
+ const {Component, values, state, getBaseProps, getTabListProps, getWrapperProps} = useTabs({
...props,
ref,
});
@@ -34,7 +34,7 @@ function Tabs(props: Props, ref: ForwardedRef
));
- return (
+ const renderTabs = (
<>
@@ -49,6 +49,12 @@ function Tabs(props: Props, ref: ForwardedRef
>
);
+
+ if ("placement" in props || "isVertical" in props) {
+ return {renderTabs}
;
+ }
+
+ return renderTabs;
}
export type TabsProps = Props & {ref?: Ref};
diff --git a/packages/components/tabs/src/use-tabs.ts b/packages/components/tabs/src/use-tabs.ts
index bb7ceba400..aaea6f4c12 100644
--- a/packages/components/tabs/src/use-tabs.ts
+++ b/packages/components/tabs/src/use-tabs.ts
@@ -47,6 +47,16 @@ export interface Props extends Omit {
* ``
*/
classNames?: SlotsToClasses;
+ /**
+ * The position of the tabs.
+ * @default 'top'
+ */
+ placement?: "top" | "bottom" | "start" | "end";
+ /**
+ * Whether the tabs are vertical it will invalidate the placement prop when the value is true.
+ * @default false
+ */
+ isVertical?: boolean;
}
export type UseTabsProps = Props &
@@ -77,8 +87,9 @@ export function useTabs(originalProps: UseTabsProps) {
classNames,
children,
disableCursorAnimation,
- shouldSelectOnPressUp = true,
motionProps,
+ isVertical = false,
+ shouldSelectOnPressUp = true,
...otherProps
} = props;
@@ -91,15 +102,16 @@ export function useTabs(originalProps: UseTabsProps) {
children: children as CollectionChildren,
...otherProps,
});
- const {tabListProps} = useTabList(otherProps, state, domRef);
+ const {tabListProps} = useTabList(otherProps as AriaTabListProps, state, domRef);
const slots = useMemo(
() =>
tabs({
...variantProps,
className,
+ ...(isVertical ? {placement: "start"} : {}),
}),
- [objectToDeps(variantProps), className],
+ [objectToDeps(variantProps), className, isVertical],
);
const baseStyles = clsx(classNames?.base, className);
@@ -143,6 +155,18 @@ export function useTabs(originalProps: UseTabsProps) {
[baseStyles, otherProps, slots],
);
+ const placement = (variantProps as Props).placement ?? (isVertical ? "start" : "top");
+ const getWrapperProps: PropGetter = useCallback(
+ (props) => ({
+ "data-slot": "tabWrapper",
+ className: slots.wrapper({class: clsx(classNames?.wrapper, props?.className)}),
+ "data-placement": placement,
+ "data-vertical":
+ isVertical || placement === "start" || placement === "end" ? "vertical" : "horizontal",
+ }),
+ [classNames, slots, placement, isVertical],
+ );
+
const getTabListProps: PropGetter = useCallback(
(props) => ({
ref: domRef,
@@ -160,6 +184,7 @@ export function useTabs(originalProps: UseTabsProps) {
values,
getBaseProps,
getTabListProps,
+ getWrapperProps,
};
}
diff --git a/packages/components/tabs/stories/tabs.stories.tsx b/packages/components/tabs/stories/tabs.stories.tsx
index 9b0ed929db..eb0551872c 100644
--- a/packages/components/tabs/stories/tabs.stories.tsx
+++ b/packages/components/tabs/stories/tabs.stories.tsx
@@ -316,6 +316,38 @@ export const ManualKeyboardActivation = {
},
};
+export const Placement = {
+ render: StaticTemplate,
+
+ args: {
+ placement: "top",
+ },
+ argTypes: {
+ placement: {
+ options: ["top", "bottom", "start", "end"],
+ control: {
+ type: "inline-radio",
+ },
+ },
+ isVertical: {
+ type: "boolean",
+ },
+ },
+};
+
+export const Vertical = {
+ render: StaticTemplate,
+
+ args: {
+ isVertical: true,
+ },
+ argTypes: {
+ isVertical: {
+ type: "boolean",
+ },
+ },
+};
+
export const DisabledItems = {
render: StaticTemplate,
diff --git a/packages/core/theme/src/components/tabs.ts b/packages/core/theme/src/components/tabs.ts
index 6f1d28db8f..3e126deb7f 100644
--- a/packages/core/theme/src/components/tabs.ts
+++ b/packages/core/theme/src/components/tabs.ts
@@ -71,6 +71,7 @@ const tabs = tv({
// focus ring
...dataFocusVisibleClasses,
],
+ wrapper: [],
},
variants: {
variant: {
@@ -159,6 +160,22 @@ const tabs = tv({
tabContent: "transition-none",
},
},
+ placement: {
+ top: {},
+ start: {
+ tabList: "flex-col",
+ panel: "py-0 px-3",
+ wrapper: "flex",
+ },
+ end: {
+ tabList: "flex-col",
+ panel: "py-0 px-3",
+ wrapper: "flex flex-row-reverse",
+ },
+ bottom: {
+ wrapper: "flex flex-col-reverse",
+ },
+ },
},
defaultVariants: {
color: "default",