MENU
Wwoon

Floating

Popover

트리거 요소를 기준으로 열리는 팝오버. 클릭으로 토글하고 외부 클릭이나 ESC로 닫습니다.

설치

npm install @woon-ui/popover
import { Popover } from '@woon-ui/popover'
import '@woon-ui/popover/css'

개요

import { Popover } from '@woon-ui/popover'

Popover는 플러그인 등록 없이 바로 사용할 수 있는 컴파운드 컴포넌트입니다. @floating-ui/react 기반으로 위치를 자동 계산하며, 충돌 감지와 자동 뒤집기를 지원합니다.

기본 사용법

import { Popover } from '@woon-ui/popover'

<Popover.Root>
  <Popover.Trigger>
    <button>열기</button>
  </Popover.Trigger>
  <Popover.Content>
    <div>팝오버 콘텐츠</div>
  </Popover.Content>
</Popover.Root>

Preview

컴파운드 컴포넌트

Popover.Root

PropTypeDefault
openboolean
제어 모드: 열림 상태
defaultOpenbooleanfalse
비제어 모드: 초기 열림 여부
onOpenChange(open: boolean) => void
열림 상태 변경 콜백

Popover.Trigger

asChild를 사용하면 Trigger의 기능을 자식 요소에 위임합니다. 기본적으로 자체 button을 렌더합니다.

Popover.Content

PropTypeDefault
side'top'|'right'|'bottom'|'left''bottom'
팝오버가 표시되는 방향
align'start'|'center'|'end''center'
트리거를 기준으로 한 정렬
sideOffsetnumber6
트리거와의 간격 (px)
alignOffsetnumber0
정렬 방향 오프셋 (px)
avoidCollisionsbooleantrue
화면 밖으로 넘어갈 때 자동으로 반대쪽으로 뒤집습니다
collisionPaddingnumber8
화면 가장자리 여백 (px)
trapFocusbooleanfalse
활성화 시 Tab 포커스가 팝오버 내부에서만 순환합니다. 비활성화(기본) 시 Tab으로 팝오버 밖으로 나가면 자동으로 닫힙니다.

Popover.Close

asChild를 사용하면 Close의 기능을 자식 요소에 위임합니다.

위치 설정

sidealign을 조합해 12가지 방향을 지정할 수 있습니다.

<Popover.Content side="top" align="start" />
<Popover.Content side="right" align="center" />
<Popover.Content side="bottom" align="end" />

avoidCollisionstrue일 때 화면 밖으로 넘어가면 자동으로 반대쪽으로 뒤집힙니다.

Preview

제어 모드

openonOpenChange를 전달하면 외부에서 상태를 제어할 수 있습니다.

const [open, setOpen] = useState(false)

<Popover.Root open={open} onOpenChange={setOpen}>
  <Popover.Trigger>열기</Popover.Trigger>
  <Popover.Content>
    <button onClick={() => setOpen(false)}>닫기</button>
  </Popover.Content>
</Popover.Root>

비제어 모드에서는 defaultOpen으로 초기 상태만 설정합니다.

스타일

@woon-ui/popover/css로 기본 스타일이 적용됩니다. 특정 값만 바꾸려면 선택자로 override하세요.

[data-woon-popover-content] {
  background: #1a1a1a;
  color: #fff;
}

완전히 교체하려면 @woon-ui/react/css import를 제거하고 아래 CSS를 복사해서 수정하세요.

[data-woon-popover-floating] { z-index: 500; }

[data-woon-popover-content],
[data-woon-popover-content] *,
[data-woon-popover-content] *::before,
[data-woon-popover-content] *::after {
  box-sizing: border-box;
}

[data-woon-popover-content] {
  width: max-content;
  max-width: calc(100vw - 1rem);
  padding: 0.75rem 1rem;
  background: #fff;
  border-radius: 8px;
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.06),
    0 4px 16px rgba(0, 0, 0, 0.12);
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 0.875rem;
  line-height: 1.5;
  outline: none;
}

[data-woon-popover-content][data-state='open'] {
  animation: woon-popover-in 160ms cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes woon-popover-in {
  from { opacity: 0; scale: 0.97; }
  to   { opacity: 1; scale: 1; }
}

@media (prefers-reduced-motion) {
  [data-woon-popover-content] { animation: none !important; }
}

접근성

  • Trigger에 aria-expanded, aria-controls, aria-haspopup="dialog" 자동 적용
  • Content에 role="dialog" 적용
  • 외부 클릭 시 닫기
  • ESC 키로 닫기 (Dialog와 escape-stack 공유)
  • trapFocus 활성화 시 Tab 키가 팝오버 내부에서만 순환
  • 기본(trapFocus=false)에서는 Tab으로 밖으로 나가면 자동 닫힘
  • 닫힐 때 Trigger로 포커스 복귀

data 속성

속성대상
data-woon-popover-contentContent
data-stateContent"open"
data-sideContent"top" "right" "bottom" "left"
data-alignContent"start" "center" "end"
[data-woon-popover-content] {
  background: white;
  border: 1px solid #e5e5e5;
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}