Global react tooltip for clipped text with ellipsis
Tags: webdev, javascript, react
Published: 2023-11-07 15:38:42

---
title: Global react tooltip for clipped text with ellipsis
published: true
date: 2023-11-06 13:07:16 UTC
tags: webdevelopment,javascript,react
canonical_url: https://medium.com/@for.mvpetrov/global-react-tooltip-for-clipped-text-with-ellipsis-98f7d530f0c0
---

Global react tooltip for clipped text with ellipsis

![](https://cdn-images-1.medium.com/max/1024/1*4axYVfBRLuw-vZi77G51Mg.png)

Every frontend developer faces the issue of not having enough space to show all the text in the container.

![](https://cdn-images-1.medium.com/max/506/1*ZxXWRRB-kXKB6kt16i2NAA.png)

There is a common CSS solution:

```
.ellipsis {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
```

![](https://cdn-images-1.medium.com/max/439/1*6LaFCx2WxX8fLv8qh5TXEw.png)

Looks better, isn’t it? But what is the full text? In many cases it’s important for the user to know the full text. Ok, we can implement a custom **<EllipsisText/>** component, pass a text as children and track a hover event to show tooltip.

```
<EllipsisText>
  elit duis ut duis Lorem excepteur exercitation esse velit culpa sit
</EllipsisText>
```

![](https://cdn-images-1.medium.com/max/447/1*p0gqlWob4Ag3WJZOLAMWKg.png)

Now we have a tooltip, but it is shown even when the text is not clipped. The ellipsis appears when **offsetWidth** of the element is less then **scrollWidth**.

![](https://cdn-images-1.medium.com/max/497/1*r_V4ZeQ2oNyIl-oz_Q0cTg.png)

Great! We have a component which can render a text with ellipsis and show a tooltip.

```
const TodoItem = ({title}) => (
  <List.Item>
    <Typography.Title level={5}>
      <EllipsisText>
        {title}
      </EllipsisText>
    </Typography.Title>
  </Liet.Item>
);
```

Every place of the code should be wrapped with **<EllipsisText/>** component. This component wrapper is extra React node and extra DOM element.

Simple component which renders a list of entity properties

```
const TodoItem = ({date, description, state, title}) => (
  <Description>
    <Property title='title'>
      <TodoStateIcon state={state} />
      {title}
    </Property>
    <Property title='description'>
      {description}
    </Property>
    <Property title='Date'>
      {date.format('LL LTS')}
    </Property>
    <Property title='state'>
      {formatState(state)}
    </Property>  
  </Description>
);
```

becomes a mess of multiple **<EllipsisText/>** wrappers

```
const TodoItem = ({date, description, state, title}) => (
  <Description>
    <Property title='title'>
      <EllipsisText>
        <TodoStateIcon state={state} />
        {title}
      </EllipsisText>
    </Property>
    <Property title='description'>
      <EllipsisText>
        {description}
      </EllipsisText>
    </Property>
    <Property title='Date'>
      <EllipsisText>
        {date.format('LL LTS')}
      </EllipsisText>
    </Property>
    <Property title='state'>
      <EllipsisText>
        {formatState(state)}
      </EllipsisText>
    </Property>  
  </Description>
);
```

### Global tooltip for text with ellipsis

The suggestion is to use a component which tracks all mouse-hovers on elements with ellipsis and shows a tooltip if the text is clipped. In the **<TodoItem/>** we can just add CSS styles with ellipsis for property values without any changes in initial component code.

#### EllipsisTooltips component

Add a listener to document’s **mouseover** event to track all child-hover events.

```
  React.useEffect(() => {
    const mouseOverHandler = (e) => setHoveredElement(e.target);

    document.addEventListener('mouseover', mouseOverHandler);
    return () => {
      document.removeEventListener('mouseover', mouseOverHandler);    
    }
  }, [setHoveredElement]);
```

Hover event can be raised on child of element with ellipsis, so we need to check target and all its ancestors for being clipped text.

```
function findBaseTooltipElement(element) {
  const elementStyle = getComputedStyle(element);
  if (isStyleWithEllipsis(elementStyle)) {
    return isOverflowX(element) ? element : null;
  }
  if (isStyleWithClamp(elementStyle)) {
    return isOverflowY(element) ? element : null;
  }
  return element.parentElement ? findBaseTooltipElement(element.parentElement) : null;
}
```

The component tracks text with ellipsis and text with limited number of lines by using the CSS **line-clamp** property.

```
const isStyleWithEllipsis = (style) => style.overflowX === 'hidden' &&
  style.textOverflow === 'ellipsis' &&
  style.whiteSpace === 'nowrap';

const isStyleWithClamp = (style) => style['-webkit-line-clamp'] > 0;

const isOverflowX = (element) => element.offsetWidth < element.scrollWidth;

const isOverflowY = (element) => element.offsetHeight < element.scrollHeight;
```

**setHoveredElement** from mouseover listener checks that new element is not a child of already hovered element with a shown tooltip

```
const setHoveredElement = React.useCallback((hoveredElement) => {
  if (tooltipBaseElement.current && tooltipBaseElement.current !== hoveredElement && !tooltipBaseElement.current.contains(hoveredElement)) {
    defineTooltipBaseElement.cancel();
    tooltipBaseElement.current = null;
    onShowTooltip?.(null);
  }
  defineTooltipBaseElement(hoveredElement);
}, [onShowTooltip, defineTooltipBaseElement]);
```

and calls **defineTooltipBaseElement** function which tries to find should the hovered element have a tooltip. The function is debounced to avoid tooltip flickering and delay tooltip show.

```
const defineTooltipBaseElement = React.useCallback(debounce((element) => {
  const baseElement = findBaseTooltipElement(element);
  tooltipBaseElement.current = baseElement;
  onShowTooltip?.(baseElement);
}, debounceTimeMilliseconds), []);
```

The full code of **<EllipsisTooltips/>** component

```
import React from 'react';
import { debounce } from 'lodash';

function findBaseTooltipElement(element) {
  const elementStyle = getComputedStyle(element);
  if (isStyleWithEllipsis(elementStyle)) {
    return isOverflowX(element) ? element : null;
  }
  if (isStyleWithClamp(elementStyle)) {
    return isOverflowY(element) ? element : null;
  }
  return element.parentElement ? findBaseTooltipElement(element.parentElement) : null;
}

const isStyleWithEllipsis = (style) => style.overflowX === 'hidden' &&
  style.textOverflow === 'ellipsis' &&
  style.whiteSpace === 'nowrap';

const isStyleWithClamp = (style) => style['-webkit-line-clamp'] > 0;

const isOverflowX = (element) => element.offsetWidth < element.scrollWidth;

const isOverflowY = (element) => element.offsetHeight < element.scrollHeight;

export const EllipsisTooltips = React.memo(({
  debounceTimeMilliseconds = 300,
  onShowTooltip
}) => {
  const tooltipBaseElement = React.useRef(null);

  const defineTooltipBaseElement = React.useCallback(debounce((element) => {
    const baseElement = findBaseTooltipElement(element);
    tooltipBaseElement.current = baseElement;
    onShowTooltip?.(baseElement);
  }, debounceTimeMilliseconds), []);
  
  const setHoveredElement = React.useCallback((hoveredElement) => {
    if (tooltipBaseElement.current && tooltipBaseElement.current !== hoveredElement && !tooltipBaseElement.current.contains(hoveredElement)) {
      defineTooltipBaseElement.cancel();
      tooltipBaseElement.current = null;
      onShowTooltip?.(null);
    }
    defineTooltipBaseElement(hoveredElement);
  }, [onShowTooltip, defineTooltipBaseElement]);

  React.useEffect(() => {
    const mouseOverHandler = (e) => setHoveredElement(e.target);

    document.addEventListener('mouseover', mouseOverHandler);
    return () => {
      document.removeEventListener('mouseover', mouseOverHandler);    
    }
  }, [setHoveredElement]);

  return null;
});
```

Developer should just define an onShowTooltip callback in props to show a tooltip. Every DOM node with clipped text with ellipsis will have a tooltip on hover.

Demo shows complex content in first block with ellipsis, 10 random filled blocks with line-clamp and 10 random filled blocks with ellipsis.

![](https://cdn-images-1.medium.com/max/522/1*20D_j3Rt8DWQxT5Hw7lSwA.gif)

Github repository with example [is here](https://github.com/mvp-v/react-ellipsis).

[Codesandbox playground](https://codesandbox.io/s/ellipsis-tooltip-yclvwg)