# 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 순서를 조정합니다.