diff --git a/src/app/(tabs)/index.tsx b/src/app/(tabs)/index.tsx
index f1fbb3b..d7a6562 100644
--- a/src/app/(tabs)/index.tsx
+++ b/src/app/(tabs)/index.tsx
@@ -1,7 +1,7 @@
import { shadows } from "@/shared/styles/shadows";
+import { SmallButton } from "@/shared/ui/buttons/ActionButton";
import { BenefitSelectionGroup } from "@/shared/ui/buttons/BenefitSelectionGroup";
import { MediumButton } from "@/shared/ui/buttons/SubmitButton";
-import { SmallButton } from "@/shared/ui/buttons/ActionButton";
import { ScrollView, Text, View } from "react-native";
export default function HomeScreen() {
diff --git a/src/shared/lib/hooks/useFadeSlideVisibility.ts b/src/shared/lib/hooks/useFadeSlideVisibility.ts
new file mode 100644
index 0000000..553f62c
--- /dev/null
+++ b/src/shared/lib/hooks/useFadeSlideVisibility.ts
@@ -0,0 +1,58 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import { Animated, Easing } from "react-native";
+
+export type UseFadeSlideVisibilityOptions = {
+ /** 애니메이션 지속 시간 (ms) */
+ duration?: number;
+ /** 숨김 시 translateY (0 = 보임, 이 값 = 숨김) */
+ translateYFrom?: number;
+};
+
+const DEFAULT_DURATION_MS = 180;
+const DEFAULT_TRANSLATE_Y_FROM = 10;
+
+/**
+ * fade + slide up visibility 애니메이션 훅.
+ * visible이 true면 opacity 0→1, translateY from→0으로 나타나고,
+ * false면 반대로 사라진 뒤 shouldRender가 false가 되어 언마운트됩니다.
+ */
+export function useFadeSlideVisibility(
+ visible: boolean,
+ options?: UseFadeSlideVisibilityOptions,
+) {
+ const duration = options?.duration ?? DEFAULT_DURATION_MS;
+ const translateYFrom = options?.translateYFrom ?? DEFAULT_TRANSLATE_Y_FROM;
+
+ const [shouldRender, setShouldRender] = useState(visible);
+ const progress = useRef(new Animated.Value(visible ? 1 : 0)).current;
+
+ useEffect(() => {
+ if (visible) {
+ setShouldRender(true);
+ }
+
+ Animated.timing(progress, {
+ toValue: visible ? 1 : 0,
+ duration,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }).start(({ finished }) => {
+ if (!finished) return;
+ if (!visible) setShouldRender(false);
+ });
+ }, [progress, visible, duration]);
+
+ const animatedStyle = useMemo(() => {
+ const translateY = progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [translateYFrom, 0],
+ });
+
+ return {
+ opacity: progress,
+ transform: [{ translateY }],
+ } as const;
+ }, [progress, translateYFrom]);
+
+ return { shouldRender, animatedStyle };
+}
diff --git a/src/widgets/chat/bottom-snackbar/README.md b/src/widgets/chat/bottom-snackbar/README.md
new file mode 100644
index 0000000..559d63c
--- /dev/null
+++ b/src/widgets/chat/bottom-snackbar/README.md
@@ -0,0 +1,70 @@
+# BottomSnackbar
+
+스택(flex) 레이아웃으로 쌓이는 스낵바 위젯입니다. fade + slide 애니메이션으로 나타나고 사라집니다.
+
+**위치는 absolute가 아닌 flex 스택으로, 부모에서 자식 순서와 `className`(padding, gap 등)으로 제어합니다.**
+
+## 사용법
+
+### Import
+
+```tsx
+import { BottomSnackbar } from "@/widgets/chat/bottom-snackbar";
+```
+
+### 기본 구조 (채팅 입력창 위에 배치)
+
+부모 `View`에서 flex 순서로 위치를 제어합니다. 스낵바를 입력창 **위**에 두려면 DOM 순서상 입력창 **앞**에 배치합니다.
+
+```tsx
+const [visible, setVisible] = useState(false);
+
+
+ ...
+
+ {/* 하단 영역: 스낵바 → 입력창 순으로 쌓임 */}
+
+ setVisible(false)}>확인
+ }
+ />
+
+
+
+```
+
+### 버튼 2개
+
+```tsx
+
+ setVisible(false)}>취소
+ 저장
+
+ }
+/>
+```
+
+## Props
+
+| Prop | 타입 | 필수 | 설명 |
+|------|------|------|------|
+| `visible` | `boolean` | ✅ | 노출 여부 (외부 상태로 제어) |
+| `title` | `string` | ✅ | 제목 |
+| `subtitle` | `string` | | 부제목 |
+| `actions` | `ReactNode` | | 하단 액션 영역 (버튼 1개/2개 등) |
+| `testID` | `string` | | 테스트용 id |
+
+## 참고
+
+- **위치 제어**: absolute 사용 안 함. 부모 `View`의 자식 순서와 `className`(padding, gap 등)으로 배치합니다.
+- **애니메이션**: `visible`이 `false`가 되어도 사라지는 애니메이션이 끝난 뒤에만 언마운트됩니다.
+- **actions**: `SmallButton`, `MediumButton` 등 공통 버튼 컴포넌트를 사용할 수 있습니다.
diff --git a/src/widgets/chat/bottom-snackbar/index.ts b/src/widgets/chat/bottom-snackbar/index.ts
new file mode 100644
index 0000000..a197188
--- /dev/null
+++ b/src/widgets/chat/bottom-snackbar/index.ts
@@ -0,0 +1,3 @@
+export { BottomSnackbar } from "./ui/BottomSnackbar";
+export type { BottomSnackbarProps } from "./model/types";
+
diff --git a/src/widgets/chat/bottom-snackbar/model/types.ts b/src/widgets/chat/bottom-snackbar/model/types.ts
new file mode 100644
index 0000000..5af46b1
--- /dev/null
+++ b/src/widgets/chat/bottom-snackbar/model/types.ts
@@ -0,0 +1,22 @@
+import type { ReactNode } from "react";
+
+export type BottomSnackbarProps = {
+ /** 노출 여부 (외부 상태로 제어) */
+ visible: boolean;
+
+ /** 제목 */
+ title: string;
+
+ /** 부제목 */
+ subtitle?: string;
+
+ /**
+ * 하단 액션 영역.
+ * 버튼 1개/2개 등 원하는 UI를 그대로 넣습니다.
+ */
+ actions?: ReactNode;
+
+ /** 테스트용 id */
+ testID?: string;
+};
+
diff --git a/src/widgets/chat/bottom-snackbar/ui/BottomSnackbar.tsx b/src/widgets/chat/bottom-snackbar/ui/BottomSnackbar.tsx
new file mode 100644
index 0000000..81baf45
--- /dev/null
+++ b/src/widgets/chat/bottom-snackbar/ui/BottomSnackbar.tsx
@@ -0,0 +1,39 @@
+import { Animated, Text, View } from "react-native";
+import { useFadeSlideVisibility } from "@/shared/lib/hooks/useFadeSlideVisibility";
+import { shadows } from "@/shared/styles/shadows";
+import type { BottomSnackbarProps } from "../model/types";
+
+export function BottomSnackbar({
+ visible,
+ title,
+ subtitle,
+ actions,
+ testID,
+}: BottomSnackbarProps) {
+ const { shouldRender, animatedStyle } = useFadeSlideVisibility(visible);
+
+ if (!shouldRender) return null; //visible false가 실행 되더라도 애니메이션은 끝까지 실행시킨 후 사라지게 함
+
+ return (
+
+
+ {title}
+ {!!subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {!!actions && {actions}}
+
+
+ );
+}
+