# Help Bubble
URL: /lynx/components/help-bubble
Source: https://github.com/daangn/seed-design/blob/dev/docs/content/lynx/components/help-bubble.mdx
사용자에게 컴포넌트의 상태나 특정 기능에 대한 추가 정보를 제공하는 말풍선입니다.
사용 가능 버전: @seed-design/lynx-react@0.8.0, @seed-design/lynx-css@0.12.0
## Preview
```tsx
import "./styles";
import IconILowercaseSerifCircleFill from "@karrotmarket/lynx-monochrome-icon/IconILowercaseSerifCircleFill";
import { ActionButton, Icon, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleTrigger } from "@/components/ui/help-bubble";
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
} />
);
}
```
## Installation
- npm: npx @seed-design/cli@latest add ui:help-bubble
- pnpm: pnpm dlx @seed-design/cli@latest add ui:help-bubble
- yarn: yarn dlx @seed-design/cli@latest add ui:help-bubble
- bun: bun x @seed-design/cli@latest add ui:help-bubble
## Usage
설치한 스니펫은 `HelpBubbleTrigger`와 `HelpBubbleAnchor`로 위치 기준점, 말풍선 콘텐츠, 화살표, 선택적인 닫기 버튼을 함께 조립합니다.
```tsx
import IconILowercaseSerifCircleFill from "@karrotmarket/lynx-monochrome-icon/IconILowercaseSerifCircleFill";
import { ActionButton, Icon } from "@seed-design/lynx-react";
import { HelpBubbleTrigger } from "@/components/ui/help-bubble";
export function App() {
return (
} />
);
}
```
- `HelpBubbleTrigger`는 자식을 탭하면 말풍선을 열고 닫습니다.
- `HelpBubbleAnchor`는 위치 기준점만 만듭니다. `defaultOpen`으로 초기 열림 상태를 정하거나, `open`과 `onOpenChange`로 열림 상태를 직접 제어할 수 있습니다.
- 기본 배치는 `"top"`입니다. `placement`, `flip`, `gutter`, `overflowPadding`, `arrowPadding`으로 위치를 조정할 수 있습니다.
- `showCloseButton`을 지정하면 기본 닫기 아이콘을 포함한 닫기 버튼을 추가합니다.
- `contentProps.maxWidth`의 기본값은 `280px`이고, `"none"`으로 최대 너비 제한을 없앨 수 있습니다. `contentProps.style.width`를 함께 지정했을 때는 `maxWidth`가 더 좁으면 `maxWidth`가 적용됩니다.
- `zIndexOffset`은 Positioner의 기본 z-index `99`에 더합니다.
## Props
### `HelpBubbleTrigger`
### `HelpBubbleAnchor`
## Examples
### Trigger
`HelpBubbleTrigger`를 탭하면 말풍선이 열리고 닫힙니다. 이 예제는 처음 열린 uncontrolled Trigger와 `open`, `onOpenChange`로 상태를 제어하는 Trigger를 함께 보여줍니다. 두 경우 모두 닫기 버튼으로 닫을 수 있습니다.
```tsx
import "./styles";
import { useState } from "@lynx-js/react";
import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleTrigger } from "@/components/ui/help-bubble";
import { Switch } from "@/components/ui/switch";
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [isControlledHelpBubbleOpen, setIsControlledHelpBubbleOpen] = useState(true);
function handleControlledOpenChange(nextOpen: boolean) {
"background only";
setIsControlledHelpBubbleOpen(nextOpen);
}
return (
토글
토글
);
}
```
### Anchor
`HelpBubbleAnchor`는 아바타처럼 말풍선의 위치만 정하는 요소이며, 탭으로 열고 닫히지 않습니다. `defaultOpen`을 쓰는 uncontrolled Anchor와 `open`, `onOpenChange`를 쓰는 controlled Anchor의 열림 상태는 각각 닫기 버튼과 `열림` Switch로 바꿉니다.
```tsx
import "./styles";
import { useState } from "@lynx-js/react";
import { Box, Text, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor } from "@/components/ui/help-bubble";
import { Switch } from "@/components/ui/switch";
const AVATAR_SRC = "https://avatars.githubusercontent.com/u/54893898?v=4";
function Avatar() {
const [hasImageError, setHasImageError] = useState(false);
function handleImageError() {
"background only";
setHasImageError(true);
}
return (
{hasImageError ? (
L
) : (
)}
);
}
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [isControlledHelpBubbleOpen, setIsControlledHelpBubbleOpen] = useState(true);
function handleControlledOpenChange(nextOpen: boolean) {
"background only";
setIsControlledHelpBubbleOpen(nextOpen);
}
return (
);
}
```
### Close On Interact Outside
`closeOnInteractOutside`의 기본값은 `true`입니다. native에서 `true`인 말풍선의 첫 바깥 탭은 말풍선만 닫고 아래 요소에는 전달되지 않습니다. `false`이면 말풍선은 열린 채로 유지되고, 바깥 탭은 아래 요소에 그대로 전달됩니다.
```tsx
import "./styles";
import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleTrigger } from "@/components/ui/help-bubble";
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
토글
토글
);
}
```
### Placement
`placement`로 기준 요소의 12개 방향에 말풍선을 배치합니다. 이 예제는 각 배치를 열린 상태로 표시하며, `flip={false}`로 지정한 방향을 유지합니다.
```tsx
import "./styles";
import IconSparkle2 from "@karrotmarket/lynx-multicolor-icon/IconSparkle2";
import { Box, HStack, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor, type HelpBubbleAnchorProps } from "@/components/ui/help-bubble";
function PlacementAnchor({
placement,
}: {
placement: NonNullable;
}) {
return (
);
}
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
);
}
```
### Flip
`flip={false}`를 지정하면 화면 경계에서 공간이 부족해도 말풍선의 방향을 바꾸지 않습니다.
```tsx
import "./styles";
import IconSparkle2 from "@karrotmarket/lynx-multicolor-icon/IconSparkle2";
import { VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor } from "@/components/ui/help-bubble";
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
);
}
```
### Close Button
`showCloseButton`으로 말풍선에 닫기 버튼을 추가할 수 있습니다. 닫기 버튼을 탭하면 말풍선이 닫히고, Trigger를 다시 탭하면 다시 엽니다.
```tsx
import "./styles";
import { ActionButton, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleTrigger } from "@/components/ui/help-bubble";
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
토글
);
}
```
### Description
`description`을 사용하여 `title` 아래에 설명을 추가할 수 있습니다.
```tsx
import "./styles";
import IconSparkle2 from "@karrotmarket/lynx-multicolor-icon/IconSparkle2";
import { VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor } from "@/components/ui/help-bubble";
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
);
}
```
### Title Only
`description` 없이 `title`만 전달할 수 있습니다.
```tsx
import "./styles";
import IconSparkle2 from "@karrotmarket/lynx-multicolor-icon/IconSparkle2";
import { VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor } from "@/components/ui/help-bubble";
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
);
}
```
### Setting Width Manually
Content에는 기본 최대 너비가 있습니다. `contentProps.maxWidth`로 이 값을 덮어쓰고, `"none"`으로 최대 너비 제한을 없앨 수 있습니다. `contentProps.style.width`를 함께 지정했을 때 `maxWidth`가 더 좁으면 `maxWidth`가 우선합니다.
```tsx
import "./styles";
import { useState } from "@lynx-js/react";
import { Text, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor } from "@/components/ui/help-bubble";
import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";
const WIDTH_OPTIONS = ["200px", "300px", "unset"] as const;
const MAX_WIDTH_OPTIONS = ["200px", "400px", "none"] as const;
type Width = (typeof WIDTH_OPTIONS)[number];
type MaxWidth = (typeof MAX_WIDTH_OPTIONS)[number];
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [width, setWidth] = useState("unset");
const [maxWidth, setMaxWidth] = useState("400px");
function handleWidthChange(nextWidth: string) {
"background only";
setWidth(nextWidth as Width);
}
function handleMaxWidthChange(nextMaxWidth: string) {
"background only";
setMaxWidth(nextMaxWidth as MaxWidth);
}
return (
width
{WIDTH_OPTIONS.map((option) => (
{option}
))}
maxWidth
{MAX_WIDTH_OPTIONS.map((option) => (
{option}
))}
);
}
```
### Line Breaks
React의 `
`는 Lynx title에서 native `` 자식과 `"\n"`으로 변환합니다. 문자열의 줄바꿈 문자도 title에 전달할 수 있습니다.
```tsx
import "./styles";
import IconSparkle2 from "@karrotmarket/lynx-multicolor-icon/IconSparkle2";
import { HStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor } from "@/components/ui/help-bubble";
const explicitLineBreakTitle = (
{"Breaking"}
{"\n"}
{"lines"}
{"\n"}
{"using"}
{"\n"}
{"`
`s"}
);
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
return (
);
}
```
### `z-index` Offset
`zIndexOffset`으로 Positioner의 기본 z-index `99`에 값을 더합니다. 이 예제는 SegmentedControl로 offset을 바꾸며 말풍선의 현재 z-index를 확인합니다.
```tsx
import "./styles";
import { useState } from "@lynx-js/react";
import { Box, HStack, Text, VStack, useSeedClassName } from "@seed-design/lynx-react";
import { HelpBubbleAnchor } from "@/components/ui/help-bubble";
import { SegmentedControl, SegmentedControlItem } from "@/components/ui/segmented-control";
const AVATAR_SRC = "https://avatars.githubusercontent.com/u/54893898?v=4";
const OFFSET_OPTIONS = ["0", "1", "2", "3", "4", "5"] as const;
function Avatar() {
const [hasImageError, setHasImageError] = useState(false);
function handleImageError() {
"background only";
setHasImageError(true);
}
return (
{hasImageError ? (
L
) : (
)}
);
}
export default function Example() {
const seedClassName = useSeedClassName({ colorMode: "system" });
const [zIndexOffset, setZIndexOffset] = useState(5);
function handleOffsetChange(nextOffset: string) {
"background only";
setZIndexOffset(Number(nextOffset));
}
return (
{Array.from({ length: 5 }, (_, index) => (
{index + 100}
))}
{OFFSET_OPTIONS.map((option) => (
{option}
))}
0
5
);
}
```
## 웹 버전과의 차이
- Lynx의 Trigger와 Anchor는 자식을 native `view`로 감쌉니다. DOM `asChild`, HTML ARIA 속성, 키보드 포커스·ESC 닫힘, portal은 제공하지 않습니다. 필요한 접근성은 Lynx의 `accessibility-*` prop과 host의 native 접근성 흐름으로 확인하세요.
- `closeOnInteractOutside`가 `true`이면 첫 번째 바깥 탭은 말풍선만 닫고 아래 요소로 전달되지 않습니다. `false`이면 말풍선은 열린 채로 바깥 탭이 아래 요소로 전달됩니다.
- Positioner는 portal이나 fullscreen overlay가 아닌 고정 native `view`입니다. `zIndexOffset`으로 같은 화면의 형제 요소와의 z-index 순서를 조정합니다.