diff --git a/.changeset/chilly-pumas-search.md b/.changeset/chilly-pumas-search.md
new file mode 100644
index 00000000..cbe67c1c
--- /dev/null
+++ b/.changeset/chilly-pumas-search.md
@@ -0,0 +1,5 @@
+---
+"wowds-ui": patch
+---
+
+Tab 컴포넌트를 구현합니다.
diff --git a/apps/wow-docs/app/page.tsx b/apps/wow-docs/app/page.tsx
index d89460d2..a3e944cb 100644
--- a/apps/wow-docs/app/page.tsx
+++ b/apps/wow-docs/app/page.tsx
@@ -10,6 +10,10 @@ import RadioButton from "wowds-ui/RadioButton";
import RadioGroup from "wowds-ui/RadioGroup";
import SearchBar from "wowds-ui/SearchBar";
import Switch from "wowds-ui/Switch";
+import Tabs from "wowds-ui/Tabs";
+import TabsContent from "wowds-ui/TabsContent";
+import TabsItem from "wowds-ui/TabsItem";
+import TabsList from "wowds-ui/TabsList";
const Home = () => {
return (
@@ -43,6 +47,22 @@ const Home = () => {
+
+
+ 첫번째첫번째첫번째첫번째
+ 두 번째
+ 세 번쨰
+
+
+ 첫번째 탭
+
+
+ 두번째 탭
+
+
+ 세번째 탭
+
+
>
);
};
diff --git a/packages/wow-ui/package.json b/packages/wow-ui/package.json
index 8d6e35a2..4d9a4f53 100644
--- a/packages/wow-ui/package.json
+++ b/packages/wow-ui/package.json
@@ -50,6 +50,26 @@
"require": "./dist/Tag.cjs",
"import": "./dist/Tag.js"
},
+ "./Tabs": {
+ "types": "./dist/components/Tabs/index.d.ts",
+ "require": "./dist/Tabs.cjs",
+ "import": "./dist/Tabs.js"
+ },
+ "./TabsContent": {
+ "types": "./dist/components/Tabs/TabsContent.d.ts",
+ "require": "./dist/TabsContent.cjs",
+ "import": "./dist/TabsContent.js"
+ },
+ "./TabsItem": {
+ "types": "./dist/components/Tabs/TabsItem.d.ts",
+ "require": "./dist/TabsItem.cjs",
+ "import": "./dist/TabsItem.js"
+ },
+ "./TabsList": {
+ "types": "./dist/components/Tabs/TabsList.d.ts",
+ "require": "./dist/TabsList.cjs",
+ "import": "./dist/TabsList.js"
+ },
"./Switch": {
"types": "./dist/components/Switch/index.d.ts",
"require": "./dist/Switch.cjs",
diff --git a/packages/wow-ui/rollup.config.js b/packages/wow-ui/rollup.config.js
index b0a3ef70..59f5108d 100644
--- a/packages/wow-ui/rollup.config.js
+++ b/packages/wow-ui/rollup.config.js
@@ -26,6 +26,10 @@ export default {
TextField: "./src/components/TextField",
TextButton: "./src/components/TextButton",
Tag: "./src/components/Tag",
+ Tabs: "./src/components/Tabs",
+ TabsContent: "./src/components/Tabs/TabsContent",
+ TabsItem: "./src/components/Tabs/TabsItem",
+ TabsList: "./src/components/Tabs/TabsList",
Switch: "./src/components/Switch",
Stepper: "./src/components/Stepper",
BlueSpinner: "./src/components/Spinner/BlueSpinner",
diff --git a/packages/wow-ui/src/components/Checkbox/index.tsx b/packages/wow-ui/src/components/Checkbox/index.tsx
index 6e8169b7..325cd822 100644
--- a/packages/wow-ui/src/components/Checkbox/index.tsx
+++ b/packages/wow-ui/src/components/Checkbox/index.tsx
@@ -116,7 +116,7 @@ const Checkbox = forwardRef(
})}
{...inputProps}
value={value}
- onClick={() => handleClick(value)}
+ onChange={() => handleClick(value)}
/>
{checked && (
(
ref={ref}
type="checkbox"
value={value}
- onClick={() => handleClick(value)}
+ onChange={() => handleClick(value)}
{...inputProps}
/>
diff --git a/packages/wow-ui/src/components/Tabs/Tabs.stories.tsx b/packages/wow-ui/src/components/Tabs/Tabs.stories.tsx
new file mode 100644
index 00000000..23020448
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/Tabs.stories.tsx
@@ -0,0 +1,196 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { useState } from "react";
+
+import type { TabsProps } from "@/components/Tabs";
+import Tabs from "@/components/Tabs";
+import TabsContent from "@/components/Tabs/TabsContent";
+import TabsItem from "@/components/Tabs/TabsItem";
+import TabsList from "@/components/Tabs/TabsList";
+
+const meta: Meta = {
+ title: "UI/Tabs",
+ component: Tabs,
+ tags: ["autodocs"],
+ parameters: {
+ componentSubtitle: "탭을 통해 콘텐츠를 선택할 수 있는 컴포넌트입니다.",
+ docs: {
+ description: {
+ component:
+ "TabsList 로 TabsItem을 감싸서 탭 트리거를 관리하고 TabsContent 로 탭 콘텐츠를 관리합니다.",
+ },
+ },
+ a11y: {
+ config: {
+ rules: [{ id: "color-contrast", enabled: false }],
+ },
+ },
+ },
+ argTypes: {
+ children: {
+ description: "TabsList,TabsItem,TabsContent 를 children 으로 받습니다.",
+ table: {
+ type: { summary: "ReactNode" },
+ },
+ control: false,
+ },
+ value: {
+ description: "현재 선택된 탭의 값을 나타냅니다.",
+ table: {
+ type: { summary: "string" },
+ },
+ control: "text",
+ },
+ defaultValue: {
+ description: "초기 선택된 탭 값을 나타냅니다.",
+ table: {
+ type: { summary: "string" },
+ },
+ control: "text",
+ },
+ onChange: {
+ description: "탭 값이 변경될 때 호출되는 함수입니다.",
+ table: {
+ type: { summary: "(value: string) => void" },
+ },
+ action: "changed",
+ },
+ label: {
+ description: "각 탭을 구분할 수 있는 레이블입니다.",
+ table: {
+ type: { summary: "string" },
+ },
+ control: "text",
+ },
+ style: {
+ description: "탭의 커스텀 스타일을 설정합니다.",
+ table: {
+ type: { summary: "CSSProperties" },
+ defaultValue: { summary: "{}" },
+ },
+ control: false,
+ },
+ className: {
+ description: "탭에 전달하는 커스텀 클래스를 설정합니다.",
+ table: {
+ type: { summary: "string" },
+ },
+ control: {
+ type: "text",
+ },
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ children: (
+ <>
+
+ Tab 1
+ Tab 2
+
+ Tab 1 Content
+ Tab 2 Content
+ >
+ ),
+ defaultValue: "tab1",
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: "기본적인 탭 컴포넌트입니다. 탭 1과 탭 2가 제공됩니다.",
+ },
+ },
+ },
+};
+
+export const WithDefaultValue: Story = {
+ args: {
+ children: (
+ <>
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ Tab 1 Content
+ Tab 2 Content
+ Tab 3 Content
+ >
+ ),
+ defaultValue: "tab2",
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "초기 값으로 두 번째 탭이 선택된 상태로 시작하는 컴포넌트입니다.",
+ },
+ },
+ },
+};
+
+const ControlledTabsComponent = () => {
+ const [selectedTab, setSelectedTab] = useState("tab1");
+
+ const handleChange = (value: string) => {
+ setSelectedTab(value);
+ };
+
+ return (
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ Tab 1 Content
+ Tab 2 Content
+ Tab 3 Content
+
+ );
+};
+
+export const ControlledValue: Story = {
+ render: () => ,
+ parameters: {
+ docs: {
+ description: {
+ story: "외부 상태에 따라 제어되는 탭 컴포넌트입니다.",
+ },
+ },
+ },
+};
+
+export const ManyTabs: Story = {
+ args: {
+ children: (
+ <>
+
+ {Array.from({ length: 10 }, (_, index) => (
+
+ Tab {index + 1}
+
+ ))}
+
+ {Array.from({ length: 10 }, (_, index) => (
+
+ Tab {index + 1} Content
+
+ ))}
+ >
+ ),
+ defaultValue: "tab1",
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: "여러 개의 탭을 가진 탭 컴포넌트입니다.",
+ },
+ },
+ },
+};
diff --git a/packages/wow-ui/src/components/Tabs/Tabs.test.tsx b/packages/wow-ui/src/components/Tabs/Tabs.test.tsx
new file mode 100644
index 00000000..32b9c992
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/Tabs.test.tsx
@@ -0,0 +1,87 @@
+import type { RenderResult } from "@testing-library/react";
+import { render, waitFor } from "@testing-library/react";
+import { userEvent } from "@testing-library/user-event";
+
+import type { TabsProps } from "@/components/Tabs";
+import Tabs from "@/components/Tabs";
+import TabsContent from "@/components/Tabs/TabsContent";
+import TabsItem from "@/components/Tabs/TabsItem";
+import TabsList from "@/components/Tabs/TabsList";
+
+describe("Tabs component", () => {
+ const uncontrolledTabs = (props: Partial = {}): RenderResult => {
+ return render(
+
+
+ Tab 1
+ Tab 2
+
+ Tab 1 Content
+ Tab 2 Content
+
+ );
+ };
+
+ const controlledTabs = (props: Partial = {}): RenderResult => {
+ return render(
+
+
+ Tab 1
+ Tab 2
+
+ Tab 1 Content
+ Tab 2 Content
+
+ );
+ };
+ test("renders correctly with default value", async () => {
+ const { getByText } = uncontrolledTabs();
+ expect(getByText("Tab 1 Content")).toBeVisible();
+ });
+
+ test("switches content when clicking on tab triggers", async () => {
+ const { getByText } = uncontrolledTabs();
+ await userEvent.click(getByText("Tab 2"));
+ await waitFor(() => {
+ expect(getByText("Tab 2 Content")).toBeVisible();
+ });
+ });
+
+ test("calls onChange when the tab is changed", async () => {
+ const handleChange = jest.fn();
+ const { getByText } = controlledTabs({
+ value: "tab1",
+ onChange: handleChange,
+ });
+ await userEvent.click(getByText("Tab 2"));
+ expect(handleChange).toHaveBeenCalledWith("tab2");
+ });
+
+ test("can navigate between tabs using keyboard (ArrowRight)", async () => {
+ const { getByText } = uncontrolledTabs();
+ const tab1 = getByText("Tab 1");
+ const tab2 = getByText("Tab 2");
+
+ tab1.focus();
+ await userEvent.keyboard("{ArrowRight}");
+ expect(tab2).toHaveFocus();
+
+ await waitFor(() => {
+ expect(getByText("Tab 2 Content")).toBeVisible();
+ });
+ });
+
+ test("can navigate between tabs using keyboard (ArrowLeft)", async () => {
+ const { getByText } = uncontrolledTabs();
+ const tab1 = getByText("Tab 1");
+ const tab2 = getByText("Tab 2");
+
+ tab1.focus();
+ await userEvent.keyboard("{ArrowLeft}");
+ expect(tab2).toHaveFocus();
+
+ await waitFor(() => {
+ expect(getByText("Tab 2 Content")).toBeVisible();
+ });
+ });
+});
diff --git a/packages/wow-ui/src/components/Tabs/TabsContent.tsx b/packages/wow-ui/src/components/Tabs/TabsContent.tsx
new file mode 100644
index 00000000..ce6234a5
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/TabsContent.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import type { PropsWithChildren } from "react";
+import { forwardRef } from "react";
+
+import type { DefaultProps } from "@/types/DefaultProps";
+
+import { useTabsContext } from "./contexts/TabsContext";
+
+/**
+ * @description TabsContent 컴포넌트는 각 Tab에 해당하는 콘텐츠입니다.
+ * @param {string} value - TabTrigger의 value와 일치하는 값입니다.
+ * @param {string} [className] - TabsContent에 전달할 커스텀 클래스.
+ * @param {CSSProperties} [style] - TabsContent에 전달할 커스텀 스타일.
+ * @param {ComponentPropsWithoutRef} rest 렌더링된 요소 또는 컴포넌트에 전달할 추가 props.
+ * @param {ComponentPropsWithRef["ref"]} ref 렌더링된 요소 또는 컴포넌트에 연결할 ref.
+ * @param {ReactNode} children - TabsContent의 자식 요소.
+ */
+interface TabsContentProps extends PropsWithChildren, DefaultProps {
+ value: string;
+}
+
+const TabsContent = forwardRef(
+ ({ value: tabValue, children }: TabsContentProps, ref) => {
+ const { value, label } = useTabsContext();
+ const selected = tabValue === value;
+ if (!selected) return null;
+
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+export default TabsContent;
diff --git a/packages/wow-ui/src/components/Tabs/TabsItem.tsx b/packages/wow-ui/src/components/Tabs/TabsItem.tsx
new file mode 100644
index 00000000..92e15ca2
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/TabsItem.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { cva } from "@styled-system/css";
+import { clsx } from "clsx";
+import type { ButtonHTMLAttributes, PropsWithChildren } from "react";
+import { forwardRef, useEffect, useRef } from "react";
+
+import { useMergeRefs } from "@/hooks/useMergeRefs";
+import type { DefaultProps } from "@/types/DefaultProps";
+
+import { useCollectionContext } from "./contexts/CollectionContext";
+import { useTabsContext } from "./contexts/TabsContext";
+
+/**
+ * @description TabsItem 컴포넌트는 각 Tab 컴포넌트입니다.
+ * @param {string} value - TabsContent의 value와 일치하는 값입니다.
+ * @param {ReactNode} children - TabsContent 자식 요소.
+ * @param {string} [className] - TabsItem에 전달할 커스텀 클래스.
+ * @param {CSSProperties} [style] - TabsItem에 전달할 커스텀 스타일.
+ * @param {ComponentPropsWithoutRef} rest 렌더링된 요소 또는 컴포넌트에 전달할 추가 props.
+ * @param {ComponentPropsWithRef["ref"]} ref 렌더링된 요소 또는 컴포넌트에 연결할 ref.
+ * @param {ReactNode} children - TabsItem의 자식 요소.
+ */
+interface TabsItemProps
+ extends PropsWithChildren,
+ DefaultProps,
+ ButtonHTMLAttributes {
+ value: string;
+}
+
+const TabsItem = forwardRef(
+ ({ value, children, className, ...rest }: TabsItemProps, ref) => {
+ const { value: selectedValue, setSelectedValue, label } = useTabsContext();
+ const selected = selectedValue === value;
+
+ const handleClickTabTrigger = () => {
+ setSelectedValue(value);
+ };
+
+ const { values } = useCollectionContext();
+ const internalButtonRef = useRef(null);
+ const buttonRef = useMergeRefs(ref, internalButtonRef);
+
+ useEffect(() => {
+ values.add(value);
+ if (selected && internalButtonRef.current) {
+ internalButtonRef.current.focus();
+ }
+ }, [values, selected, value]);
+
+ return (
+
+ );
+ }
+);
+export default TabsItem;
+
+const tabItemStyle = cva({
+ base: {
+ textStyle: "label1",
+ paddingY: "sm",
+ paddingX: "14px",
+ borderBottom: "1px solid",
+ borderColor: "outline",
+ color: "sub",
+ outline: "none",
+ cursor: "pointer",
+ whiteSpace: "pre",
+ xsToSm: {
+ display: "flex",
+ flexGrow: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ },
+ variants: {
+ type: {
+ selected: {
+ color: "primary",
+ borderColor: "primary",
+ },
+ default: {},
+ },
+ },
+});
diff --git a/packages/wow-ui/src/components/Tabs/TabsList.tsx b/packages/wow-ui/src/components/Tabs/TabsList.tsx
new file mode 100644
index 00000000..ed02fee9
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/TabsList.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { css } from "@styled-system/css";
+import { Flex } from "@styled-system/jsx";
+import {
+ type KeyboardEvent,
+ type PropsWithChildren,
+ useCallback,
+ useEffect,
+} from "react";
+
+import { useCollectionContext } from "./contexts/CollectionContext";
+import { useTabsContext } from "./contexts/TabsContext";
+
+/**
+ * @description TabsList 컴포넌트는 TabsItem 컴포넌트들을 관리합니다.
+ */
+const TabsList = ({ children }: PropsWithChildren) => {
+ const {
+ label,
+ setSelectedValue,
+ value: selectedValue,
+ isControlled,
+ } = useTabsContext();
+
+ const { values } = useCollectionContext();
+
+ useEffect(() => {
+ if (!isControlled && !selectedValue && values.size > 0) {
+ setSelectedValue(values.values().next().value);
+ }
+ }, []);
+
+ const updateFocusedValue = useCallback(
+ (direction: number) => {
+ const valuesArray = Array.from(values);
+ const currentIndex = valuesArray.indexOf(selectedValue ?? "");
+ const nextIndex =
+ (currentIndex + direction + valuesArray.length) % valuesArray.length;
+ setSelectedValue(valuesArray[nextIndex] ?? "");
+ },
+ [setSelectedValue, selectedValue, values]
+ );
+
+ const handleArrowNavigation = useCallback(
+ (direction: number, event: KeyboardEvent) => {
+ updateFocusedValue(direction);
+ event.preventDefault();
+ },
+ [updateFocusedValue]
+ );
+
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ const { key } = event;
+
+ if (key === "ArrowRight") {
+ handleArrowNavigation(1, event);
+ } else if (key === "ArrowLeft") {
+ handleArrowNavigation(-1, event);
+ }
+ },
+ [handleArrowNavigation]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default TabsList;
+
+const tabsListStyle = css({
+ overflowX: "scroll",
+ scrollBehavior: "smooth",
+ _scrollbar: {
+ width: "65px",
+ height: "2px",
+ },
+ _scrollbarThumb: {
+ width: "65px",
+ height: "2px",
+ borderRadius: "sm",
+ backgroundColor: "outline",
+ },
+ _scrollbarTrack: {
+ marginTop: "2px",
+ marginBottom: "2px",
+ },
+});
diff --git a/packages/wow-ui/src/components/Tabs/contexts/CollectionContext.tsx b/packages/wow-ui/src/components/Tabs/contexts/CollectionContext.tsx
new file mode 100644
index 00000000..f0ce0217
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/contexts/CollectionContext.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import type { PropsWithChildren } from "react";
+import { createContext } from "react";
+
+import useSafeContext from "@/hooks/useSafeContext";
+
+interface CollectionContextProps {
+ values: Set;
+}
+
+const CollectionContext = createContext(null);
+
+export const useCollectionContext = () => {
+ const context = useSafeContext(CollectionContext);
+ return context;
+};
+
+export const CollectionProvider = ({ children }: PropsWithChildren) => {
+ return (
+ () }}>
+ {children}
+
+ );
+};
diff --git a/packages/wow-ui/src/components/Tabs/contexts/TabsContext.ts b/packages/wow-ui/src/components/Tabs/contexts/TabsContext.ts
new file mode 100644
index 00000000..60259df4
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/contexts/TabsContext.ts
@@ -0,0 +1,19 @@
+"use client";
+
+import { createContext } from "react";
+
+import useSafeContext from "@/hooks/useSafeContext";
+
+interface TabsContextProps {
+ value: string;
+ setSelectedValue: (value: string) => void;
+ label?: string;
+ isControlled: boolean;
+}
+
+export const TabsContext = createContext(null);
+
+export const useTabsContext = () => {
+ const context = useSafeContext(TabsContext);
+ return context;
+};
diff --git a/packages/wow-ui/src/components/Tabs/index.tsx b/packages/wow-ui/src/components/Tabs/index.tsx
new file mode 100644
index 00000000..50edce44
--- /dev/null
+++ b/packages/wow-ui/src/components/Tabs/index.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import { css } from "@styled-system/css";
+import { clsx } from "clsx";
+import type { PropsWithChildren } from "react";
+import { useRef, useState } from "react";
+
+import { CollectionProvider } from "@/components/Tabs/contexts/CollectionContext";
+import { TabsContext } from "@/components/Tabs/contexts/TabsContext";
+import type { DefaultProps } from "@/types/DefaultProps";
+
+/**
+ * @description Tabs 컴포넌트는 탭을 통해 콘텐츠를 선택할 수 있는 컴포넌트입니다.
+ * @param {string} [defaultValue] - 탭의 기본값입니다.
+ * @param {string} [value] - 현재 선택된 탭의 값입니다.
+ * @param {string} [label] - 각 탭을 구분할 수 있는 레이블입니다.
+ * @param {(value: string) => void} [onChange] - 탭이 변경될 때 호출되는 함수입니다.
+ * @param {CSSProperties} [style] - 탭 컴포넌트의 커스텀 스타일.
+ * @param {string} [className] - 탭 컴포넌트에 전달할 커스텀 클래스.
+ * @param {ReactNode} children - 탭의 자식 요소.
+ */
+export interface TabsProps extends PropsWithChildren, DefaultProps {
+ defaultValue?: string;
+ value?: string;
+ label?: string;
+ onChange?: (value: string) => void;
+}
+const Tabs = ({
+ defaultValue,
+ value: valueProp,
+ label = "default-tab",
+ children,
+ onChange,
+ className,
+ style,
+}: TabsProps) => {
+ const [selectedValue, setSelectedValue] = useState(defaultValue ?? "");
+ const isControlled = useRef(valueProp !== undefined).current;
+
+ const handleSelect = (selectedValue: string) => {
+ if (!isControlled) {
+ setSelectedValue(selectedValue);
+ return;
+ }
+ if (onChange) {
+ onChange(selectedValue);
+ }
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default Tabs;
+
+const tabsContainerStyle = css({
+ width: "100%",
+});
diff --git a/packages/wow-ui/src/hooks/useMergeRefs.ts b/packages/wow-ui/src/hooks/useMergeRefs.ts
new file mode 100644
index 00000000..11b535d8
--- /dev/null
+++ b/packages/wow-ui/src/hooks/useMergeRefs.ts
@@ -0,0 +1,13 @@
+import type { MutableRefObject, Ref } from "react";
+
+export function useMergeRefs(...refs: (Ref | null)[]) {
+ return (value: T | null) => {
+ refs.forEach((ref) => {
+ if (typeof ref === "function") {
+ ref(value);
+ } else if (ref !== null && typeof ref === "object") {
+ (ref as MutableRefObject).current = value;
+ }
+ });
+ };
+}
diff --git a/packages/wow-ui/src/types/DefaultProps.ts b/packages/wow-ui/src/types/DefaultProps.ts
new file mode 100644
index 00000000..ace10e37
--- /dev/null
+++ b/packages/wow-ui/src/types/DefaultProps.ts
@@ -0,0 +1,11 @@
+import type { CSSProperties } from "react";
+
+/**
+ * @description 컴포넌트에 전달한 기본적으로 전달한 props 입니다.
+ * @property {string} className - 컴포넌트에 전달할 커스텀 클래스명입니다.
+ * @property {CSSProperties} style - 컴포넌트에 전달할 커스텀 스타일입니다.
+ */
+export interface DefaultProps {
+ className?: string;
+ style?: CSSProperties;
+}