Popup

사용자의 즉각적인 주의와 응답이 필요한 상황에서 사용하는 모달 요소입니다. 사용자의 현재 작업을 중단 시키고 팝업 내용에 집중하도록 하며, 필수적인 결정이나 확인이 필요한 경우에 사용합니다.

Basic popup

Popup overlay를 표시합니다.

breakpoint에 따라 variant를 override 하여 Bottom sheet 와 쉽게 통합할 수 있습니다.

import {
  Button,
  Modal,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalDescription,
  ModalHeading,
  ModalNavigation,
  ModalSummary,
  ModalTrigger,
} from '@montage-ui/core';

const Demo = () => {
  return (
    <Modal>
      <ModalTrigger>
        <Button>Open</Button>
      </ModalTrigger>

      <ModalContainer variant="popup">
        <ModalNavigation>
          Title
        </ModalNavigation>

        <ModalContent>
          <ModalContentItem>
            <ModalHeading>Heading</ModalHeading>
            <ModalSummary>Summary</ModalSummary>
            <ModalDescription>Description</ModalDescription>
          </ModalContentItem>
        </ModalContent>

        <ActionArea>
          <ActionAreaButton>
            Action
          </ActionAreaButton>
        </ActionArea>
      </ModalContainer>
    </Modal>
  )
}

export default Demo;

Anatomy

Popup 은 여러 컴포넌트를 조합해서 사용합니다.

ModalTrigger 는 선택적으로 사용할 수 있습니다.

기본 구성은 아래와 같습니다.

<Modal>
  <ModalTrigger>
    <Button />
  </ModalTrigger>

  <ModalContainer>
    <ModalNavigation />

    <ModalContent>
      <ModalContentItem>
        <ModalHeading />
        <ModalSummary />
        <ModalDescription />
      </ModalContentItem>
    </ModalContent>

    <ActionArea>
      <ActionAreaButton />
    </ActionArea>
  </ModalContainer>
</Modal>

Sizes

3가지 사이즈를 사용할 수 있습니다.

  • medium (default)
  • large
  • xlarge
import {
  FlexBox,
  Button,
  Modal,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalDescription,
  ModalHeading,
  ModalNavigation,
  ModalSummary,
  ModalTrigger,
} from '@montage-ui/core';

const Demo = () => {
  return (
    <FlexBox gap="12px" flexWrap="wrap">
      <Modal>
        <ModalTrigger>
          <Button>Medium</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" size="medium">
          <ModalNavigation>
            Title
          </ModalNavigation>

          <ModalContent>
            <ModalContentItem>
              <ModalHeading>Heading</ModalHeading>
              <ModalSummary>Summary</ModalSummary>
              <ModalDescription>Description</ModalDescription>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>

      <Modal>
        <ModalTrigger>
          <Button>Large</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" size="large">
          <ModalNavigation>
            Title
          </ModalNavigation>

          <ModalContent>
            <ModalContentItem>
              <ModalHeading>Heading</ModalHeading>
              <ModalSummary>Summary</ModalSummary>
              <ModalDescription>Description</ModalDescription>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>

      <Modal>
        <ModalTrigger>
          <Button>Xlarge</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" size="xlarge">
          <ModalNavigation>
            Title
          </ModalNavigation>

          <ModalContent>
            <ModalContentItem>
              <ModalHeading>Heading</ModalHeading>
              <ModalSummary>Summary</ModalSummary>
              <ModalDescription>Description</ModalDescription>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>
    </FlexBox>
  )
}

export default Demo;

Resize

resize 옵션을 사용하면 Popup이 차지하는 높이를 조정할 수 있습니다.

  • hug (default)
  • fixed
import {
  FlexBox,
  Button,
  Modal,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalDescription,
  ModalHeading,
  ModalNavigation,
  ModalSummary,
  ModalTrigger,
} from '@montage-ui/core';

const Demo = () => {
  return (
    <FlexBox gap="12px" flexWrap="wrap">
      <Modal>
        <ModalTrigger>
          <Button>Hug</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" resize="hug">
          <ModalNavigation>
            Hug
          </ModalNavigation>

          <ModalContent>
            <ModalContentItem>
              <ModalHeading>Heading</ModalHeading>
              <ModalSummary>Summary</ModalSummary>
              <ModalDescription>Description</ModalDescription>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>

      <Modal>
        <ModalTrigger>
          <Button>Fixed</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" resize="fixed">
          <ModalNavigation>
            Fixed
          </ModalNavigation>

          <ModalContent>
            <ModalContentItem>
              <ModalHeading>Heading</ModalHeading>
              <ModalSummary>Summary</ModalSummary>
              <ModalDescription>Description</ModalDescription>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>
    </FlexBox>
  )
}

export default Demo;

3가지 variant의 Navigation을 사용할 수 있습니다.

  • emphasized (default)
  • floating
  • search

또한 ModalNavigation 의 leadingContent, trailingContent로 콘텐츠를 추가할 수 있으며 ModalNavigationButton 으로 사용합니다.

variant
button variant
leadingContent
trailingContent
import {
  FlexBox,
  Box,
  Button,
  Modal,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalNavigation,
  ModalNavigationButton,
  ModalTrigger,
  SearchField,
  SegmentedControl,
  SegmentedControlItem,
  Typography,
} from '@montage-ui/core';
import { useState } from 'react';

const Control = ({ label, value, onValueChange, options }) => (
  <FlexBox flexDirection="column" gap="8px" sx={{ width: '100%', maxWidth: 400 }}>
    <Typography variant="label1" weight="medium" color="semantic.foreground.neutral.secondary">
      {label}
    </Typography>
    <SegmentedControl size="small" value={value} onValueChange={onValueChange}>
      {options.map((option) => (
        <SegmentedControlItem key={option} value={option}>
          {option}
        </SegmentedControlItem>
      ))}
    </SegmentedControl>
  </FlexBox>
);

const Demo = () => {
  const [navigationVariant, setNavigationVariant] = useState('emphasized');
  const [buttonVariant, setButtonVariant] = useState('icon-button');
  const [leading, setLeading] = useState('on');
  const [trailing, setTrailing] = useState('on');
  const [background, setBackground] = useState('off');

  // background only applies to icon buttons in a floating navigation
  const supportsBackground = navigationVariant === 'floating' && buttonVariant === 'icon-button';
  const iconButtonBackground = supportsBackground ? background === 'on' : undefined;

  const leadingContent = leading === 'on' ? (
    <ModalNavigationButton variant={buttonVariant === 'icon-button' ? 'back-button' : buttonVariant} background={iconButtonBackground}>
      {buttonVariant === 'text-button' ? '뒤로' : undefined}
    </ModalNavigationButton>
  ) : null;

  const trailingContent = trailing === 'on' ? (
    <ModalNavigationButton variant={buttonVariant === 'icon-button' ? 'close-button' : buttonVariant} background={iconButtonBackground}>
      {buttonVariant === 'text-button' ? '닫기' : undefined}
    </ModalNavigationButton>
  ) : null;

  const getNavigationContent = () => {
    switch (navigationVariant) {
      case 'search':
        return <SearchField placeholder="Enter search keyword." />;
      case 'floating':
        return null;
      case 'emphasized':
        return 'Emphasized';
    }
  };

  return (
    <FlexBox flexDirection="column" alignItems="center" gap="24px" sx={{ width: '100%' }}>
      <Control
        label="variant"
        value={navigationVariant}
        onValueChange={setNavigationVariant}
        options={['emphasized', 'floating', 'search']}
      />
      <Control
        label="button variant"
        value={buttonVariant}
        onValueChange={setButtonVariant}
        options={['icon-button', 'text-button']}
      />
      <Control label="leadingContent" value={leading} onValueChange={setLeading} options={['on', 'off']} />
      <Control label="trailingContent" value={trailing} onValueChange={setTrailing} options={['on', 'off']} />
      {supportsBackground && (
        <Control
          label="button background"
          value={background}
          onValueChange={setBackground}
          options={['on', 'off']}
        />
      )}

      <Modal>
        <ModalTrigger>
          <Button>Open</Button>
        </ModalTrigger>

        <ModalContainer variant="popup">
          <ModalNavigation
            variant={navigationVariant}
            leadingContent={leadingContent}
            trailingContent={trailingContent}
          >
            {getNavigationContent()}
          </ModalNavigation>

          <ModalContent>
            <ModalContentItem>
              <Box
                sx={theme => ({
                  width: '100%',
                  height: '800px',
                  background: theme.semantic.surface.accent.violetOpaque,
                  opacity: 0.08
                })}
              />
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>
    </FlexBox>
  )
}

export default Demo;

Close button

닫기 버튼을 없애거나 좌측에 배치할 수도 있습니다.

import {
  Button,
  Modal,
  ModalTrigger,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalDescription,
  ModalHeading,
  ModalNavigation,
  ModalNavigationButton,
  ModalSummary,
} from '@montage-ui/core';

const Demo = () => {

  return (
    <Modal>
       <ModalTrigger>
        <Button>Open</Button>
      </ModalTrigger>

      <ModalContainer variant="popup">
        <ModalNavigation trailingContent={null} leadingContent={<ModalNavigationButton variant="close-button" />}>
          Title
        </ModalNavigation>

        <ModalContent>
          <ModalContentItem>
            <ModalHeading>Heading</ModalHeading>
            <ModalSummary>Summary</ModalSummary>
            <ModalDescription>Description</ModalDescription>
          </ModalContentItem>
        </ModalContent>

        <ActionArea>
          <ActionAreaButton>
            Action
          </ActionAreaButton>
        </ActionArea>
      </ModalContainer>
    </Modal>
  )
}

export default Demo;

Content

내부 콘텐츠는 ModalContent 로 감싸서 사용하는 것을 권장하며, verticalPadding, horizontalPadding, gap 옵션을 사용하여 내부 여백을 조정할 수 있습니다.

verticalPadding

  • none (default)
  • top-only
  • bottom-only
  • both

horizontalPadding

  • none
  • both (default)
import {
  FlexBox,
  Button,
  Modal,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalDescription,
  ModalHeading,
  ModalNavigation,
  ModalSummary,
  ModalTrigger,
  Typography,
  SegmentedControl,
  SegmentedControlItem,
  addOpacity,
} from '@montage-ui/core';
import { useState } from 'react';

const Control = ({ label, value, onValueChange, options }) => (
  <FlexBox flexDirection="column" gap="8px" sx={{ width: '100%', maxWidth: 400 }}>
    <Typography variant="label1" weight="medium" color="semantic.foreground.neutral.secondary">
      {label}
    </Typography>
    <SegmentedControl size="small" value={value} onValueChange={onValueChange}>
      {options.map((option) => (
        <SegmentedControlItem key={option} value={option}>
          {option}
        </SegmentedControlItem>
      ))}
    </SegmentedControl>
  </FlexBox>
);

const Demo = () => {
  const [verticalPadding, setVerticalPadding] = useState('none');
  const [horizontalPadding, setHorizontalPadding] = useState('both');

  return (
    <FlexBox gap="12px" flexWrap="wrap">
      <Modal>
        <ModalTrigger>
          <Button>Medium</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" size="medium">
          <ModalNavigation>
            Title
          </ModalNavigation>

          <ModalContent verticalPadding={verticalPadding} horizontalPadding={horizontalPadding}>
            <ModalContentItem>
              <FlexBox
                flexDirection="column"
                gap="12px"
                alignItems="center"
                justifyContent="center"
                sx={theme => ({
                  width: '100%',
                  height: '400px',
                  background: addOpacity(theme.semantic.surface.accent.violetOpaque, 0.08),
                })}
              >
                <Control
                  label="verticalPadding"
                  value={verticalPadding}
                  onValueChange={setVerticalPadding}
                  options={['none', 'top-only', 'bottom-only', 'both']}
                />
                <Control
                  label="horizontalPadding"
                  value={horizontalPadding}
                  onValueChange={setHorizontalPadding}
                  options={['none', 'both']}
                />
              </FlexBox>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>

      <Modal>
        <ModalTrigger>
          <Button>Large</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" size="large">
          <ModalNavigation>
            Title
          </ModalNavigation>

          <ModalContent verticalPadding={verticalPadding} horizontalPadding={horizontalPadding}>
            <ModalContentItem>
              <FlexBox
                flexDirection="column"
                gap="12px"
                alignItems="center"
                justifyContent="center"
                sx={theme => ({
                  width: '100%',
                  height: '400px',
                  background: addOpacity(theme.semantic.surface.accent.violetOpaque, 0.08),
                })}
              >
                <Control
                  label="verticalPadding"
                  value={verticalPadding}
                  onValueChange={setVerticalPadding}
                  options={['none', 'top-only', 'bottom-only', 'both']}
                />
                <Control
                  label="horizontalPadding"
                  value={horizontalPadding}
                  onValueChange={setHorizontalPadding}
                  options={['none', 'both']}
                />
              </FlexBox>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>

      <Modal>
        <ModalTrigger>
          <Button>Xlarge</Button>
        </ModalTrigger>

        <ModalContainer variant="popup" size="xlarge">
          <ModalNavigation>
            Title
          </ModalNavigation>

          <ModalContent verticalPadding={verticalPadding} horizontalPadding={horizontalPadding}>
            <ModalContentItem>
              <FlexBox
                flexDirection="column"
                gap="12px"
                alignItems="center"
                justifyContent="center"
                sx={theme => ({
                  width: '100%',
                  height: '400px',
                  background: addOpacity(theme.semantic.surface.accent.violetOpaque, 0.08),
                })}
              >
                <Control
                  label="verticalPadding"
                  value={verticalPadding}
                  onValueChange={setVerticalPadding}
                  options={['none', 'top-only', 'bottom-only', 'both']}
                />
                <Control
                  label="horizontalPadding"
                  value={horizontalPadding}
                  onValueChange={setHorizontalPadding}
                  options={['none', 'both']}
                />
              </FlexBox>
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton>
              Action
            </ActionAreaButton>
          </ActionArea>
        </ModalContainer>
      </Modal>
    </FlexBox>
  )
}

export default Demo;

Customize

ModalContainer의 wrapperProps 를 사용하면 모달이 나타나는 위치를 조정할 수 있습니다.

import {
  Button,
  Modal,
  ModalTrigger,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalDescription,
  ModalHeading,
  ModalNavigation,
  ModalSummary,
} from '@montage-ui/core';

const Demo = () => {
  return (
    <Modal>
      <ModalTrigger>
        <Button>Open</Button>
      </ModalTrigger>

      <ModalContainer
        variant="popup"
        wrapperProps={{
          sx: {
            justifyContent: 'flex-end',
            alignItems: 'flex-end'
          }
        }}>
        <ModalNavigation>
          Title
        </ModalNavigation>

        <ModalContent>
          <ModalContentItem>
            <ModalHeading>Heading</ModalHeading>
            <ModalSummary>Summary</ModalSummary>
            <ModalDescription>Description</ModalDescription>
          </ModalContentItem>
        </ModalContent>

        <ActionArea>
          <ActionAreaButton>
            Action
          </ActionAreaButton>
        </ActionArea>
      </ModalContainer>
    </Modal>
  )
}

export default Demo;

With form

Form 구조로 사용하려면 ModalContainer 하위에 <FlexBox as="form" flex="1" flexDirection="column"> 구조를 사용합니다.

import {
  Button,
  Modal,
  ActionArea,
  ActionAreaButton,
  ModalContainer,
  ModalContent,
  ModalContentItem,
  ModalNavigation,
  ModalTrigger,
  TextField,
  FlexBox,
  FormControl,
  FormControlField,
  FormControlLabel,
  FormControlNegativeMessage,
  FormControlMessage,
} from '@montage-ui/core';
import { useForm, Controller } from 'react-hook-form';

const Demo = () => {
  const form = useForm({ defaultValues: { value: '' } });

  return (
    <Modal>
      <ModalTrigger>
        <Button>Open</Button>
      </ModalTrigger>

      <ModalContainer variant="popup">
        <FlexBox
          as="form"
          flex="1"
          flexDirection="column"
          onSubmit={form.handleSubmit((v) => alert(JSON.stringify(v)))}
        >
          <ModalNavigation>
            Title
          </ModalNavigation>

          <ModalContent>
            <ModalContentItem>
              <Controller
                control={form.control}
                name="value"
                rules={{
                  required: {
                    value: true,
                    message: '필수 값입니다.'
                  }
                }}
                render={({ field, formState }) => (
                  <FormControl>
                    <FormControlLabel>라벨</FormControlLabel>

                    <FormControlField>
                      <TextField {...field} width="100%" placeholder="값을 입력하세요." status={Boolean(formState.errors.value) ? 'negative' : 'normal'} />
                    </FormControlField>

                    {Boolean(formState.errors.value) ? (
                      <FormControlNegativeMessage>{formState.errors.value?.message}</FormControlNegativeMessage>
                    ): (
                      <FormControlMessage>Description</FormControlMessage>
                    )}
                  </FormControl>
                )} />
            </ModalContentItem>
          </ModalContent>

          <ActionArea>
            <ActionAreaButton type="submit">
              Submit
            </ActionAreaButton>
          </ActionArea>
        </FlexBox>
      </ModalContainer>
    </Modal>
  )
}

export default Demo;

API

NameTypesdefaultValue
open
boolean
-
defaultOpen
boolean
-
onOpenChange
(open: boolean) => void
-
children
ReactNode
-
sx
SxProp
-

ModalContainer

NameTypesdefaultValue
as
ElementType
-
variant
"full" | "bottom" | "popup"
"popup"
handle
boolean
-
peekHeight
number
-
sticky
boolean
true
size
"medium" | "large" | "xlarge"
"medium"
resize
"fill" | "fixed" | "hug" | "flexible"
"hug"
snap
"full" | "peek" | "half"
-
defaultSnap
"full" | "half"
"half"
onSnapChange
(snap: "full" | "peek" | "half") => void
-
largestUndimmedSnap
"peek" | "half"
"peek"
enableHalfSnapScroll
boolean
false
children
ReactNode
-
wrapperProps
DefaultComponentProps<{}, 'div'>
-
dimmer
ReactNode
<ModalDimmer />
container
null | Element | DocumentFragment
-
disableOutsideClickClose
boolean
false
disableEscapeKeyDownClose
boolean
false
disableRemoveScroll
boolean
false
disableFocusScope
boolean
false
disableAriaHiddenOthers
boolean
false
disablePortal
boolean
false
forceMount
boolean
false
sx
SxProp
-
xl
Merge<Pick<ModalContainerDefaultProps, "variant" | "size" | "handle" | "resize">, { sx?: CSSInterpolation; }> | undefined
-
lg
Merge<Pick<ModalContainerDefaultProps, "variant" | "size" | "handle" | "resize">, { sx?: CSSInterpolation; }> | undefined
-
md
Merge<Pick<ModalContainerDefaultProps, "variant" | "size" | "handle" | "resize">, { sx?: CSSInterpolation; }> | undefined
-
sm
Merge<Pick<ModalContainerDefaultProps, "variant" | "size" | "handle" | "resize">, { sx?: CSSInterpolation; }> | undefined
-
xs
Merge<Pick<ModalContainerDefaultProps, "variant" | "size" | "handle" | "resize">, { sx?: CSSInterpolation; }> | undefined
-

ModalNavigation

NameTypesdefaultValue
variant
"search" | "normal" | "floating" | "emphasized"
-
trailingContent
ReactNode
<ModalNavigationButton variant="close-button" />
leadingContent
ReactNode
-
toolbar
ReactNode
-
background
boolean
-
children
ReactNode
-
xl
{ sx?: CSSInterpolation; } | undefined
-
lg
{ sx?: CSSInterpolation; } | undefined
-
md
{ sx?: CSSInterpolation; } | undefined
-
sm
{ sx?: CSSInterpolation; } | undefined
-
xs
{ sx?: CSSInterpolation; } | undefined
-
sx
SxProp
-

ModalNavigationButton

NameTypesdefaultValue
as
ElementType
-
variant
"icon-button" | "text-button" | "back-button" | "close-button"
"icon-button"
color
"primary" | "assistive"
"assistive"
disabled
boolean
-
size
number | "small" | "medium" | "large" | "xlarge"
-
background
boolean
-
alternative
boolean
-
children
ReactNode
-
sx
SxProp
-

ModalContent

NameTypesdefaultValue
gap
Property.Gap<string | number> | undefined
"var(--modal-content-margin-y, 24px)"
verticalPadding
"none" | "both" | "top-only" | "bottom-only"
-
horizontalPadding
"none" | "both"
"both"
children
ReactNode
-
sx
SxProp
-
xl
Merge<Pick<ModalContentDefaultProps, "verticalPadding" | "gap" | "horizontalPadding">, { sx?: CSSInterpolation; }> | undefined
-
lg
Merge<Pick<ModalContentDefaultProps, "verticalPadding" | "gap" | "horizontalPadding">, { sx?: CSSInterpolation; }> | undefined
-
md
Merge<Pick<ModalContentDefaultProps, "verticalPadding" | "gap" | "horizontalPadding">, { sx?: CSSInterpolation; }> | undefined
-
sm
Merge<Pick<ModalContentDefaultProps, "verticalPadding" | "gap" | "horizontalPadding">, { sx?: CSSInterpolation; }> | undefined
-
xs
Merge<Pick<ModalContentDefaultProps, "verticalPadding" | "gap" | "horizontalPadding">, { sx?: CSSInterpolation; }> | undefined
-

ModalContentItem

FlexBox 와 동일한 Props를 사용합니다.

ModalHeading

Typography 와 동일한 Props를 사용합니다.

ModalSummary

Typography 와 동일한 Props를 사용합니다.

ModalDescription

Typography 와 동일한 Props를 사용합니다.

© 2026 Wanted Lab, Inc.