Tooltip
Tooltip (툴팁)
사용자가 요소 위에 마우스를 올리거나, 포커스하거나, 탭하면 정보성 텍스트를 표시하는 Tooltip 컴포넌트에 대해 알아봅니다.
출처: 문서
본문
Tooltip은 사용자가 요소 위에 마우스를 올리거나, 포커스하거나, 탭할 때 정보성 텍스트를 표시합니다.
활성화되면 Tooltip은 그 기능의 설명과 같은 요소를 식별하는 텍스트 라벨을 표시해요.
기본 tooltip (Basic tooltip)
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
export default function BasicTooltip() {
return (
<Tooltip title="Delete">
<IconButton>
<DeleteIcon />
</IconButton>
</Tooltip>
);
}
라벨과 설명 (Labels and descriptions)
기본적으로 tooltip은 자식 요소를 라벨링만 합니다. 이는 title과는 두드러지게 다른데, title은 자식이 이미 라벨을 갖고 있는지에 따라 자식을 라벨링하거나 설명할 수 있기 때문이죠. 예를 들어 아래 요소에서 title은 접근 가능한 설명으로 동작합니다.
<button title="some more information">A button</button>
tooltip이 접근 가능한 설명으로 동작하게 하려면 describeChild prop을 전달하면 됩니다. tooltip이 유일한 시각적 라벨을 제공한다면 describeChild를 사용하면 안 돼요. 그 경우 자식은 접근 가능한 이름이 없게 되고 tooltip은 WCAG 2.2 Success Criterion 2.5.3을 위반하니까요. 트리거에 이미 보이는 텍스트나 aria-label이 있다면, tooltip을 설명으로 사용하고 describeChild prop을 전달하세요. 그렇지 않으면 기본 동작을 사용해 tooltip이 트리거를 라벨링하게 할 수 있어요.
import DeleteIcon from '@mui/icons-material/Delete';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
export default function AccessibilityTooltips() {
return (
<div>
<Tooltip title="Delete">
<IconButton>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip describeChild title="Does not add if it already exists.">
<Button>Add</Button>
</Tooltip>
</div>
);
}
위치 지정 tooltip (Positioned tooltips)
Tooltip은 12가지 placement 선택지를 가집니다. 방향 화살표는 없고, 대신 소스에서 발산하는 움직임에 의존해 방향을 전달합니다.
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
export default function PositionedTooltips() {
return (
<Box sx={{ width: 500 }}>
<Stack direction="row" sx={{ justifyContent: 'center' }}>
<Tooltip describeChild title="Add" placement="top-start">
<Button>top-start</Button>
</Tooltip>
<Tooltip describeChild title="Add" placement="top">
<Button>top</Button>
</Tooltip>
<Tooltip describeChild title="Add" placement="top-end">
<Button>top-end</Button>
</Tooltip>
</Stack>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Stack direction="column" sx={{ alignItems: 'flex-start' }}>
<Tooltip describeChild title="Add" placement="left-start">
<Button>left-start</Button>
</Tooltip>
<Tooltip describeChild title="Add" placement="left">
<Button>left</Button>
</Tooltip>
<Tooltip describeChild title="Add" placement="left-end">
<Button>left-end</Button>
</Tooltip>
</Stack>
<Stack direction="column" sx={{ alignItems: 'flex-end' }}>
<Tooltip describeChild title="Add" placement="right-start">
<Button>right-start</Button>
</Tooltip>
<Tooltip describeChild title="Add" placement="right">
<Button>right</Button>
</Tooltip>
<Tooltip describeChild title="Add" placement="right-end">
<Button>right-end</Button>
</Tooltip>
</Stack>
</Box>
<Stack direction="row" sx={{ justifyContent: 'center' }}>
<Tooltip title="Add" placement="bottom-start">
<Button>bottom-start</Button>
</Tooltip>
<Tooltip title="Add" placement="bottom">
<Button>bottom</Button>
</Tooltip>
<Tooltip title="Add" placement="bottom-end">
<Button>bottom-end</Button>
</Tooltip>
</Stack>
</Box>
);
}
커스터마이즈 (Customization)
이 컴포넌트를 커스터마이즈하는 몇 가지 예시입니다. 이에 대해 더 배우려면 overrides documentation page를 참고하세요.
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip describeChild {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.common.white,
color: 'rgba(0, 0, 0, 0.87)',
boxShadow: theme.shadows[1],
fontSize: 11,
},
}));
const BootstrapTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip describeChild {...props} arrow classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.arrow}`]: {
color: theme.palette.common.black,
},
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.common.black,
},
}));
const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip describeChild {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: '#f5f5f9',
color: 'rgba(0, 0, 0, 0.87)',
maxWidth: 220,
fontSize: theme.typography.pxToRem(12),
border: '1px solid #dadde9',
},
}));
export default function CustomizedTooltips() {
return (
<div>
<LightTooltip title="Add">
<Button>Light</Button>
</LightTooltip>
<BootstrapTooltip title="Add">
<Button>Bootstrap</Button>
</BootstrapTooltip>
<HtmlTooltip
title={
<React.Fragment>
<Typography
sx={{
color: 'inherit',
}}
>
Tooltip with HTML
</Typography>
<em>{"And here's"}</em> <b>{'some'}</b> <u>{'amazing content'}</u>.{' '}
{"It's very engaging. Right?"}
</React.Fragment>
}
>
<Button>HTML</Button>
</HtmlTooltip>
</div>
);
}
화살표 tooltip (Arrow tooltips)
arrow prop을 사용해 tooltip이 참조하는 요소를 가리키는 화살표를 줄 수 있어요.
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
export default function ArrowTooltips() {
return (
<Tooltip describeChild title="Add" arrow>
<Button>Arrow</Button>
</Tooltip>
);
}
앵커와의 거리 (Distance from anchor)
tooltip과 앵커 사이의 거리를 조정하려면 slotProps prop을 사용해 popper의 offset을 수정하면 됩니다.
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
export default function TooltipOffset() {
return (
<Tooltip
describeChild
title="Add"
slotProps={{
popper: {
modifiers: [
{
name: 'offset',
options: {
offset: [0, -14],
},
},
],
},
}}
>
<Button>Offset</Button>
</Tooltip>
);
}
또는 slotProps prop을 사용해 popper의 margin을 커스터마이즈할 수도 있어요.
import Button from '@mui/material/Button';
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip';
export default function TooltipMargin() {
return (
<Tooltip
title="Add"
describeChild
slotProps={{
popper: {
sx: {
[`&.${tooltipClasses.popper}[data-popper-placement*="bottom"] .${tooltipClasses.tooltip}`]:
{
marginTop: '0px',
},
[`&.${tooltipClasses.popper}[data-popper-placement*="top"] .${tooltipClasses.tooltip}`]:
{
marginBottom: '0px',
},
[`&.${tooltipClasses.popper}[data-popper-placement*="right"] .${tooltipClasses.tooltip}`]:
{
marginLeft: '0px',
},
[`&.${tooltipClasses.popper}[data-popper-placement*="left"] .${tooltipClasses.tooltip}`]:
{
marginRight: '0px',
},
},
},
}}
>
<Button>Margin</Button>
</Tooltip>
);
}
커스텀 자식 요소 (Custom child element)
tooltip은 자식 요소에 DOM 이벤트 리스너를 적용해야 합니다. 자식이 커스텀 React 요소라면, props를 기본 DOM 요소로 spread하는지 확인해야 해요.
const MyComponent = React.forwardRef(function MyComponent(props, ref) {
// Spread the props to the underlying DOM element.
return (
<div {...props} ref={ref}>
Bin
</div>
);
});
// ...
<Tooltip title="Delete">
<MyComponent />
</Tooltip>;
자식으로 클래스 컴포넌트를 사용한다면, ref가 기본 DOM 요소로 전달되는지도 확인해야 합니다. (클래스 컴포넌트 자체에 대한 ref는 작동하지 않아요.)
class MyComponent extends React.Component {
render() {
const { innerRef, ...props } = this.props;
// Spread the props to the underlying DOM element.
return (
<div {...props} ref={innerRef}>
Bin
</div>
);
}
}
// Wrap MyComponent to forward the ref as expected by Tooltip
const WrappedMyComponent = React.forwardRef(function WrappedMyComponent(props, ref) {
return <MyComponent {...props} innerRef={ref} />;
});
// ...
<Tooltip title="Delete">
<WrappedMyComponent />
</Tooltip>;
트리거 (Triggers)
tooltip을 표시하게 하는 이벤트의 타입을 정의할 수 있어요. 터치 동작은 enterTouchDelay prop이 기본적으로 700ms로 설정되어 있기 때문에 길게 누르기를 요구합니다.
import * as React from 'react';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import ClickAwayListener from '@mui/material/ClickAwayListener';
export default function TriggersTooltips() {
const [open, setOpen] = React.useState(false);
const handleTooltipClose = () => {
setOpen(false);
};
const handleTooltipOpen = () => {
setOpen(true);
};
return (
<div>
<Grid container sx={{ justifyContent: 'center' }}>
<Grid>
<Tooltip describeChild disableFocusListener title="Add">
<Button>Hover or touch</Button>
</Tooltip>
</Grid>
<Grid>
<Tooltip describeChild disableHoverListener title="Add">
<Button>Focus or touch</Button>
</Tooltip>
</Grid>
<Grid>
<Tooltip
describeChild
disableFocusListener
disableTouchListener
title="Add"
>
<Button>Hover</Button>
</Tooltip>
</Grid>
<Grid>
<ClickAwayListener onClickAway={handleTooltipClose}>
<div>
<Tooltip
describeChild
onClose={handleTooltipClose}
open={open}
disableFocusListener
disableHoverListener
disableTouchListener
title="Add"
slotProps={{
popper: {
disablePortal: true,
},
}}
>
<Button onClick={handleTooltipOpen}>Click</Button>
</Tooltip>
</div>
</ClickAwayListener>
</Grid>
</Grid>
</div>
);
}
제어되는 tooltip (Controlled tooltips)
open, onOpen, onClose props를 사용해 tooltip의 동작을 제어할 수 있어요.
import * as React from 'react';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
export default function ControlledTooltips() {
const [open, setOpen] = React.useState(false);
const handleClose = () => {
setOpen(false);
};
const handleOpen = () => {
setOpen(true);
};
return (
<Tooltip
describeChild
open={open}
onClose={handleClose}
onOpen={handleOpen}
title="Add"
>
<Button>Controlled</Button>
</Tooltip>
);
}
가변 너비 (Variable width)
Tooltip은 기본적으로 긴 텍스트를 읽을 수 있도록 줄바꿈합니다.
import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
const CustomWidthTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip describeChild {...props} classes={{ popper: className }} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 500,
},
});
const NoMaxWidthTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip describeChild {...props} classes={{ popper: className }} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 'none',
},
});
const longText = `
Aliquam eget finibus ante, non facilisis lectus. Sed vitae dignissim est, vel aliquam tellus.
Praesent non nunc mollis, fermentum neque at, semper arcu.
Nullam eget est sed sem iaculis gravida eget vitae justo.
`;
export default function VariableWidth() {
return (
<div>
<Tooltip describeChild title={longText}>
<Button sx={{ m: 1 }}>Default Width [300px]</Button>
</Tooltip>
<CustomWidthTooltip title={longText}>
<Button sx={{ m: 1 }}>Custom Width [500px]</Button>
</CustomWidthTooltip>
<NoMaxWidthTooltip title={longText}>
<Button sx={{ m: 1 }}>No wrapping</Button>
</NoMaxWidthTooltip>
</div>
);
}
인터랙티브 (Interactive)
Tooltip은 기본적으로 인터랙티브합니다(WCAG 2.2 Success Criterion 1.4.13을 통과하기 위해서죠). 사용자가 leaveDelay가 만료되기 전에 tooltip 위로 마우스를 올리면 닫히지 않습니다. disableInteractive를 전달하면 이 동작을 비활성화할 수 있어요(Level AA 달성에 필요한 성공 기준을 충족하지 못하게 됩니다).
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
export default function NonInteractiveTooltips() {
return (
<Tooltip describeChild title="Add" disableInteractive>
<Button>Not interactive</Button>
</Tooltip>
);
}
비활성 요소 (Disabled elements)
기본적으로 <button> 같은 비활성 요소는 사용자 상호작용을 트리거하지 않으므로 Tooltip은 hover 같은 일반 이벤트에서 활성화되지 않습니다. 비활성 요소를 지원하려면 span 같은 간단한 래퍼 요소를 추가하세요.
:::warning Safari에서 작동하려면 tooltip 래퍼 아래에 display block 또는 flex 항목이 하나 이상 필요합니다. :::
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
export default function DisabledTooltips() {
return (
<Tooltip describeChild title="You don't have permission to do this">
<span>
<Button disabled>A Disabled Button</Button>
</span>
</Tooltip>
);
}
:::warning
ButtonBase에서 상속받지 않는 Material UI 컴포넌트(예: 네이티브 <button> 요소)를 감싸는 경우가 아니라면, 비활성화될 때 요소에 CSS 속성 _pointer-events: none;_을 추가해야 합니다.
:::
<Tooltip describeChild title="You don't have permission to do this">
<span>
<button disabled={disabled} style={disabled ? { pointerEvents: 'none' } : {}}>
A disabled button
</button>
</span>
</Tooltip>
전환 (Transitions)
다른 전환을 사용하려면 slots.transition과 slotProps.transition을 쓰면 돼요.
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import Fade from '@mui/material/Fade';
import Zoom from '@mui/material/Zoom';
export default function TransitionsTooltips() {
return (
<div>
<Tooltip describeChild title="Add">
<Button>Grow</Button>
</Tooltip>
<Tooltip
describeChild
title="Add"
slots={{
transition: Fade,
}}
slotProps={{
transition: { timeout: 600 },
}}
>
<Button>Fade</Button>
</Tooltip>
<Tooltip
describeChild
title="Add"
slots={{
transition: Zoom,
}}
>
<Button>Zoom</Button>
</Tooltip>
</div>
);
}
커서 따라가기 (Follow cursor)
followCursor={true}를 설정하면 tooltip이 커서를 따라가도록 할 수 있어요.
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
export default function FollowCursorTooltips() {
return (
<Tooltip describeChild title="You don't have permission to do this" followCursor>
<Box sx={{ bgcolor: 'text.disabled', color: 'background.paper', p: 2 }}>
Disabled Action
</Box>
</Tooltip>
);
}
가상 요소 (Virtual element)
커스텀 배치를 구현해야 할 경우 anchorEl prop을 사용할 수 있어요. anchorEl prop의 값은 가짜 DOM 요소에 대한 참조일 수 있습니다. VirtualElement 형태의 객체를 만들어야 해요.
import * as React from 'react';
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
import { Instance } from '@popperjs/core';
export default function AnchorElTooltips() {
const positionRef = React.useRef<{ x: number; y: number }>({
x: 0,
y: 0,
});
const popperRef = React.useRef<Instance>(null);
const areaRef = React.useRef<HTMLDivElement>(null);
const handleMouseMove = (event: React.MouseEvent) => {
positionRef.current = { x: event.clientX, y: event.clientY };
if (popperRef.current != null) {
popperRef.current.update();
}
};
return (
<Tooltip
describeChild
title="Add"
placement="top"
arrow
slotProps={{
popper: {
popperRef,
anchorEl: {
getBoundingClientRect: () => {
return new DOMRect(
positionRef.current.x,
areaRef.current!.getBoundingClientRect().y,
0,
0,
);
},
},
},
}}
>
<Box
ref={areaRef}
onMouseMove={handleMouseMove}
sx={{ bgcolor: 'primary.main', color: 'primary.contrastText', p: 2 }}
>
Hover
</Box>
</Tooltip>
);
}
표시와 숨김 (Showing and hiding)
tooltip은 보통 사용자의 마우스가 요소 위로 올라가면 즉시 표시되고, 마우스가 떠나면 즉시 숨겨집니다. enterDelay와 leaveDelay props로 표시/숨김에 지연을 추가할 수 있어요.
모바일에서는 사용자가 요소를 길게 누르면 tooltip이 표시되고 1500ms의 지연 후 숨겨집니다. disableTouchListener prop으로 이 기능을 비활성화할 수 있어요.
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
export default function DelayTooltips() {
return (
<Tooltip describeChild title="Add" enterDelay={500} leaveDelay={200}>
<Button>[500ms, 200ms]</Button>
</Tooltip>
);
}
접근성 (Accessibility)
(WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/)
Tooltip은 포커스 가능하고 hover 가능한 트리거(예: 버튼)를 감싸서 모든 사용자가 활성화할 수 있게 해야 합니다. Tooltip이 표시되면 트리거에 자동으로 연결됩니다. 트리거 요소는 tooltip 콘텐츠에 의해 라벨링되거나 설명됩니다. 하지만 tooltip 콘텐츠는 잘려진(truncated) 콘텐츠의 완전한 텍스트 대안으로 사용해서는 안 됩니다.
Tooltip API
Demos
이 React 컴포넌트의 사용 예시와 자세한 내용은 컴포넌트 데모 페이지에서 확인할 수 있어요.
Import
import Tooltip from '@mui/material/Tooltip';
// or
import { Tooltip } from '@mui/material';
Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| children | element |
- | Yes | |
| arrow | bool |
false |
No | |
| classes | object |
- | No | Override or extend the styles applied to the component. |
| describeChild | bool |
false |
No | |
| disableFocusListener | bool |
false |
No | |
| disableHoverListener | bool |
false |
No | |
| disableInteractive | bool |
false |
No | |
| disableTouchListener | bool |
false |
No | |
| enterDelay | number |
100 |
No | |
| enterNextDelay | number |
0 |
No | |
| enterTouchDelay | number |
700 |
No | |
| followCursor | bool |
false |
No | |
| id | string |
- | No | |
| leaveDelay | number |
0 |
No | |
| leaveTouchDelay | number |
1500 |
No | |
| onClose | function(event: React.SyntheticEvent) => void |
- | No | |
| onOpen | function(event: React.SyntheticEvent) => void |
- | No | |
| open | bool |
- | No | |
| placement | 'auto-end' | 'auto-start' | 'auto' | 'bottom-end' | 'bottom-start' | 'bottom' | 'left-end' | 'left-start' | 'left' | 'right-end' | 'right-start' | 'right' | 'top-end' | 'top-start' | 'top' |
'bottom' |
No | |
| slotProps | { arrow?: func | object, popper?: func | object, tooltip?: func | object, transition?: func | object } |
{} |
No | |
| slots | { arrow?: elementType, popper?: elementType, tooltip?: elementType, transition?: elementType } |
{} |
No | |
| sx | Array<func | object | bool> | func | object |
- | No | The system prop that allows defining system overrides as well as additional CSS styles. |
| title | node |
- | No |
참고:
ref는 루트 요소 (HTMLButtonElement).
그 외에 제공된 props는 루트 요소 (native element).
Theme default props
MuiTooltip을 사용하면 테마에서 이 컴포넌트의 기본 props를 바꿀 수 있어요.
Slots
| Name | Default | Class | Description |
|---|---|---|---|
| popper | Popper |
.MuiTooltip-popper |
The component used for the popper. |
| transition | Grow |
- | The component used for the transition. |
| Follow this guide to learn more about the requirements for this component. | |||
| tooltip | undefined |
.MuiTooltip-tooltip |
The component used for the tooltip. |
| arrow | undefined |
.MuiTooltip-arrow |
The component used for the arrow. |
CSS
Rule name
| Global class | Rule name | Description |
|---|---|---|
| - | popperArrow | Styles applied to the Popper component if arrow={true}. |
| - | popperClose | Styles applied to the Popper component unless the tooltip is open. |
| - | popperInteractive | Styles applied to the Popper component unless disableInteractive={true}. |
| - | tooltipArrow | Styles applied to the tooltip (label wrapper) element if arrow={true}. |
| - | tooltipPlacementBottom | Styles applied to the tooltip (label wrapper) element if placement contains "bottom". |
| - | tooltipPlacementLeft | Styles applied to the tooltip (label wrapper) element if placement contains "left". |
| - | tooltipPlacementRight | Styles applied to the tooltip (label wrapper) element if placement contains "right". |
| - | tooltipPlacementTop | Styles applied to the tooltip (label wrapper) element if placement contains "top". |
| - | touch | Styles applied to the tooltip (label wrapper) element if the tooltip is opened by touch. |
Source code
이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용은 컴포넌트 구현을 살펴보는 것도 좋아요.