diff --git a/src/shared/ui/TabBar/README.md b/src/shared/ui/TabBar/README.md
new file mode 100644
index 0000000..15c4160
--- /dev/null
+++ b/src/shared/ui/TabBar/README.md
@@ -0,0 +1,75 @@
+# TabBar
+
+stateless 탭바 컴포넌트입니다. 선택된 탭에 따라 underline이 슬라이드 애니메이션으로 이동합니다. 배경은 투명하여 부모 배경에 맞춰 사용할 수 있습니다.
+
+## 사용법
+
+### Import
+
+```tsx
+import { TabBar } from "@/shared/ui/TabBar";
+```
+
+### 기본 사용
+
+```tsx
+const [activeTab, setActiveTab] = useState("inquiry");
+
+
+```
+
+### 탭 3개 이상
+
+```tsx
+
+```
+
+### 탭에 따른 콘텐츠 전환
+
+```tsx
+const [activeTab, setActiveTab] = useState("inquiry");
+
+
+
+ {activeTab === "inquiry" && }
+ {activeTab === "history" && }
+
+```
+
+## Props
+
+| Prop | 타입 | 필수 | 설명 |
+|------|------|------|------|
+| `tabs` | `TabBarTab[]` | ✅ | 탭 목록 (`{ id, label }`) |
+| `activeTab` | `string` | ✅ | 현재 선택된 탭 id |
+| `onTabChange` | `(id: string) => void` | ✅ | 탭 클릭 시 호출 |
+| `testID` | `string` | | 테스트용 id |
+
+## 참고
+
+- **stateless**: `activeTab`과 `onTabChange`로 부모가 상태를 제어합니다.
+- **배경**: 투명. 부모 `View`의 `className`으로 배경색을 지정합니다.
+- **underline**: 선택된 탭으로 200ms 슬라이드 애니메이션.
+- **탭 개수**: 2개 이상 모두 지원. `flex-1`로 균등 배치됩니다.
diff --git a/src/shared/ui/TabBar/TabBar.tsx b/src/shared/ui/TabBar/TabBar.tsx
new file mode 100644
index 0000000..7dbbcca
--- /dev/null
+++ b/src/shared/ui/TabBar/TabBar.tsx
@@ -0,0 +1,47 @@
+import { Animated, Pressable, Text, View } from "react-native";
+import type { TabBarProps } from "./types";
+import { useTabBarUnderlineAnimation } from "./useTabBarUnderlineAnimation";
+
+export function TabBar({ tabs, activeTab, onTabChange, testID }: TabBarProps) {
+ const { tabWidth, translateX, handleLayout } = useTabBarUnderlineAnimation(
+ tabs,
+ activeTab,
+ );
+
+ return (
+
+
+ {tabs.map((tab) => {
+ const active = activeTab === tab.id;
+ return (
+ onTabChange(tab.id)}
+ >
+
+ {tab.label}
+
+
+ );
+ })}
+
+
+ {tabWidth > 0 && (
+
+ )}
+
+
+ );
+}
diff --git a/src/shared/ui/TabBar/index.ts b/src/shared/ui/TabBar/index.ts
new file mode 100644
index 0000000..e16d99e
--- /dev/null
+++ b/src/shared/ui/TabBar/index.ts
@@ -0,0 +1,2 @@
+export { TabBar } from "./TabBar";
+export type { TabBarProps, TabBarTab } from "./types";
diff --git a/src/shared/ui/TabBar/types.ts b/src/shared/ui/TabBar/types.ts
new file mode 100644
index 0000000..28ac800
--- /dev/null
+++ b/src/shared/ui/TabBar/types.ts
@@ -0,0 +1,11 @@
+export type TabBarTab = {
+ id: string;
+ label: string;
+};
+
+export type TabBarProps = {
+ tabs: TabBarTab[];
+ activeTab: string;
+ onTabChange: (id: string) => void;
+ testID?: string;
+};
diff --git a/src/shared/ui/TabBar/useTabBarUnderlineAnimation.ts b/src/shared/ui/TabBar/useTabBarUnderlineAnimation.ts
new file mode 100644
index 0000000..cec4524
--- /dev/null
+++ b/src/shared/ui/TabBar/useTabBarUnderlineAnimation.ts
@@ -0,0 +1,36 @@
+import { useEffect, useRef, useState } from "react";
+import { Animated, Easing, type LayoutChangeEvent } from "react-native";
+import type { TabBarTab } from "./types";
+
+const ANIMATION_DURATION_MS = 200;
+
+export function useTabBarUnderlineAnimation(tabs: TabBarTab[], activeTab: string) {
+ const [containerWidth, setContainerWidth] = useState(0);
+ const translateX = useRef(new Animated.Value(0)).current;
+ const isInitialMount = useRef(true);
+
+ const activeIndex = tabs.findIndex((tab) => tab.id === activeTab);
+ const tabWidth = containerWidth > 0 ? containerWidth / tabs.length : 0;
+ const targetTranslateX = activeIndex >= 0 ? activeIndex * tabWidth : 0;
+
+ useEffect(() => {
+ if (tabWidth <= 0) return;
+ if (isInitialMount.current) {
+ isInitialMount.current = false;
+ translateX.setValue(targetTranslateX);
+ } else {
+ Animated.timing(translateX, {
+ toValue: targetTranslateX,
+ duration: ANIMATION_DURATION_MS,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }).start();
+ }
+ }, [translateX, targetTranslateX, tabWidth]);
+
+ const handleLayout = (e: LayoutChangeEvent) => {
+ setContainerWidth(e.nativeEvent.layout.width);
+ };
+
+ return { tabWidth, translateX, handleLayout };
+}