Что такое всплывающие подсказки?

Если дословно, то сам термин «tooltips» — английского происхождения и переводится как «советы по инструментам». Под этим термином согласно Википедии понимают информационную подсказку или подсказку, в которой при наведении курсора мыши на элемент экрана в текстовом поле отображается информация об этом элементе, например, описание функции кнопки, обозначение аббревиатуры и т. д.

Всплывающая подсказка (tooltip) может выглядеть например вот так:

Вы наверняка не раз с ними сталкивались, так что, всплывающие подсказки весьма распространенная штука для интерфейсов web приложений.

React — классный фреймворк для разработки web приложений. И на нем пишет множество разработчиков, в том числе и я. А поскольку у разработчика проектов как правило много, то хорошо было бы компоненты интерфейсов в своих проектах переиспользовать, с одного в другом. Для скорости разработки, классно иметь набор таких универсальных компонентов для интерфейса. Чтобы в этих компонентах поддерживался их базовый функционал, их можно было легко стилизовать под конкретный проект и по минимуму использовать сторонние библиотеки для их легковесности.

Давайте так и поступим — напишем самостоятельно универсальный и легко переиспользуемый компонент для всплывающих подсказок.

Писать будем на React + TypeScript, для стилизации будем использовать css.modules. В дальнейшем для плавной анимации появления и исчезновения всплывающих подсказок еще подключим React библиотеку «react‑transition‑group», но это потом.

А сейчас посмотрим на то, что мы хотим сделать. Наш будущий tooltip должен будет принять в себя произвольный элемент интерфейса в виде JSX компонента React, и при наведении указателя мыши на этот целевой элемент интерфейса — показать для него всплывающую подсказку.

Для начала создадим наш новый React компонент — ToolTipComponent.

ToolTipComponent.tsx

import React from 'react';
import classes from './ToolTipComponent.module.css';

const ToolTipComponent: React.FC = () => {  
  return (    
    <div className={classes.container}>      
      ToolTip-Component    
    </div>  
  );
};

export default ToolTipComponent;

Его стилизацию определим в файле — ToolTipComponent.module.css, она у нас пока состоит всего из одного класса:

ToolTipComponent.module.css

.container {  
  display: flex;
}

Вставляем наш tooltip в компонент App:

import React from 'react';
import classes from './App.module.css';
import ToolTipComponent from './Components/ToolTipComponent/ToolTipComponent';

function App() {  
  return (    
    <div className={classes.container}>      
      <ToolTipComponent />    
    </div>  
  );
}

export default App;

Наше приложение теперь выглядит так:

Для того, чтобы внутри компонента ToolTipComponent мы могли отобразить целевой компонент при наведении на который мы хотим видеть всплывающую подсказку, мы должны передать этот целевой компонент как props — children в наш tooltip.

Так же нам понадобится текст для самой всплывающей подсказки, передадим его как props — text. Внутри компонента обернем текст подсказки в тег <div>, для того чтобы стилизовать его и спозиционировать относительно его родительского контейнера.

Теперь наш ToolTipComponent выглядит так:

ToolTipComponent.tsx

import React, { ReactElement } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
};

const ToolTipComponent: React.FC  <PropsType>= ({children, text}) => {
  return (
    <div className={classes.container}>
      {children}
      <div className={classes.tooltip}>
        {text}
      </div>
    </div>
  );
};

export default ToolTipComponent;

ToolTipComponent.module.css

.container {
  position: relative;
  display: flex;
}

.tooltip {
  position: absolute;
  width: 180px;
  padding: 4px 12px;
  margin-left: calc(100%);
  justify-content: center;
  color: #FFFFFF;
  background-color: #FF8E00;
  border-radius: 12px ;
  text-align: center;
  white-space: pre-line;
  font-weight: 700 ;
  pointer-events: none;
}

Создадим и стилизуем произвольный компонент. Я для нашего мини приложения создал ButtonComponent. Это самая обыкновенная кнопка, сделанная из тэга <div>, кнопка ничего не будет делать. Однако это будет тот целевой компонент, в который мы обернем в наш в tooltip, для того чтобы когда мы наведем указатель мыши на эту кнопку, мы могли увидеть всплывающую подсказку.

ButtonComponent.tsx

import React from 'react';
import classes from './ButtonComponent.module.css';

const ButtonComponent: React.FC = () => {  
  return (    
    <div className={classes.container}>      
      Нажми на меня    
    </div>  
  );
};

export default ButtonComponent;

Стилизация кнопки прописана здесь:

ButtonComponent.module.css

.container {  
  display: flex;  
  width: 80px;  
  justify-content: center;  
  align-items: center;  
  padding: 6px 24px;  
  text-align: center;  
  color: #FFFFFF;  
  background-color: #4A90E2;  
  font-size: 18px;  
  font-weight: 600;  
  border-radius: 12px;  
  cursor: pointer;
}

Экран нашего приложения теперь выглядит так — кнопка и рядом с ней спозиционирована наша всплывающая подсказка.

Управлять поведением нашей всплывающей подсказки мы будем через состояние React, используя hook — useState. В моменты, когда указатель мыши заходит на компонент tooltip, мы будем подсказку отображать. Как только указатель мыши компонент tooltip покидает, мы будем подсказку скрывать. Для чего используем события мыши для компонента React — «onMouseEnter» и «onMouseLeave».

Теперь наш TooTtipComponent выглядит так

ToolTipComponent.tsx

import React, { ReactElement, useState } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text }) => {
  const [showToolTip, setShowToolTip] = useState(false);

  const onMouseEnterHandler = () => {
    setShowToolTip(true);
  };

  const onMouseLeaveHandler = () => {
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      {showToolTip && <div className={classes.tooltip}>{text}</div>}
    </div>
  );
};

export default ToolTipComponent;

и теперь наш tooltip уже динамически может показывать подсказку для нашей кнопки.

Однако видно, что подсказка появляется моментально, как только указатель мыши оказывается над целевым компонентом. Это не есть хорошо, с точки зрения пользовательского опыта, потому что пользователь уже взаимодействовал с этой кнопкой ранее и знает, для чего она применяется. В этом случае пользователю подсказка не нужна, он просто хочет нажать на кнопку. Давайте предоставим пользователю эту возможность.

Поправим наш tooltip, так что бы была задержка в 0,75 секунды, перед тем как появится всплывающая подсказка. За эти 0,75 секунды, пользователь успеет нажать на кнопку и подсказка для него не появится.

Чтобы реализовать подобный функционал, мы используем метод таймера setTimeout из браузерного API. При заходе указателя мыши на целевой компонент будет запускаться функция, обеспечивающая задержку показа текста всплывающей подсказки, а в случае выхода указателя мыши за пределы целевого компонента мы будем сбрасывать таймер setTimeout и скрывать текст всплывающей подсказки. Чтобы не потерять идентификатор setTimeOut между перерендерами нашего React компонента ToolTipComponrent, мы поместим значение идентификатора метода setTimeout в поле current хука React — useRef. Поле current хука useRef является универсальным хранилищем в React компонентах для данных, которые мы хотим сохранять между их перерендерами.

И теперь наш код нашего tooltip компонента выглядит так:

ToolTipComponent.tsx

import React, { ReactElement, useRef, useState } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text }) => {
  const refSetTimeout = useRef<NodeJS.Timeout>();
  const [showToolTip, setShowToolTip] = useState(false);

  const onMouseEnterHandler = () => {
    refSetTimeout.current = setTimeout(() => {
      setShowToolTip(true);
    }, 750);
  };

  const onMouseLeaveHandler = () => {
    clearTimeout(refSetTimeout.current);
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      {showToolTip && <div className={classes.tooltip}>{text}</div>}
    </div>
  );
};

export default ToolTipComponent;

Как видно на видео, наша идея с задержкой появления текста подсказки в 0,75 секунды работает. Всплывающая подсказка появляется только спустя какое‑то время, после того как пользователь навел указатель мыши на кнопку. Если бы он нажал на кнопку или увел указатель за пределы кнопки быстрее, чем установленная нами задержка в 0,75 секунды, то всплывающая подсказка для него так бы и не появилась.

Но для того, чтобы наш компонент всплывающих подсказок был универсальным и мы могли переиспользовать его в разных проектах, нам необходимо добавить возможность стилизации всплывающей подсказки. Сделаем это через передачу еще одного props — customClass в наш компонент.

И теперь наш код компонент всплывающих подсказок выглядит так:

ToolTipComponent.tsx

import React, { ReactElement, useRef, useState } from 'react';
import classes from './ToolTipComponent.module.css';

type PropsType = {
  children: ReactElement;
  text: string;
  customClass?: string;
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text, customClass }) => {
  const refSetTimeout = useRef<NodeJS.Timeout>();
  const [showToolTip, setShowToolTip] = useState(false);
  const toolTipClasses = customClass ? `${classes.tooltip} ${customClass}` : `${classes.tooltip}`;

  const onMouseEnterHandler = () => {
    refSetTimeout.current = setTimeout(() => {
      setShowToolTip(true);
    }, 750);
  };

  const onMouseLeaveHandler = () => {
    clearTimeout(refSetTimeout.current);
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      {showToolTip && <div className={toolTipClasses}>{text}</div>}
    </div>
  );
};

export default ToolTipComponent;

Подключим.customStyle — передав его в props нашему ToolTipComponent:

App.tsx

import React from 'react';
import classes from './App.module.css';
import ToolTipComponent from './Components/ToolTipComponent/ToolTipComponent';
import ButtonComponent from './Components/ButtonComponent/ButtonComponent';

function App() {
  return (
    <div className={classes.container}>
      <ToolTipComponent text={'Я подсказка'} customClass={classes.toolTipCustom}>
        <ButtonComponent />
      </ToolTipComponent>
    </div>
  );
}

export default App;

.customClass для нашего нового стиля подсказки выглядит так:

App.module.css

.container {
  display: flex;
  padding: 64px;
}

.toolTipCustom {
  display: flex;
  top: -5px;
  left: 32px;
  height: 48px;
  padding: 8px 32px;
  align-items: center;
  background-color: #1e9f00;
  color: #fdfa65;
  border-radius: 50%;
}

Вот как теперь выглядит наша всплывающая подсказка. Она стилизована и спозиционированна точно так как мы ее описали в css классе.toolTipCustom

Казалось бы, на этом и все, мы добились чего хотели — У нас есть универсальный компонент для отображения всплывающих подсказок. В этот компонент мы можем обернуть любой элемент нашего интерфейса, передать в него текст всплывающей подсказки, стили для нее, и получить при наведении мыши на этот элемент нашу всплывающую подсказку.

И этот вполне себе уже рабочий компонент, однако совсем несложно улучшить его еще.

С помощью библиотеки «react‑transition‑group» предоствляемой командой создателей React, мы добавим к нашей всплывающей подсказке возможность появляться плавно, выезжать со стороны и затем плавно исчезать. И для придания всех этих расширенных свойств нашей всплывающей подсказке мы используем компонент «CSSTransition» из библиотеки «react‑transition‑group»

Сначала установим из саму библиотеку из npm и затем подключим «CSSTransition» в наш tooltip компонент. Заметьте, что вместо условия по которому мы раньше разрешали рендеринг подсказки, теперь импортированный компонент «CSSTransition» возьмет на себя обязанность контролировать отображение нашей всплывающей подсказки, а так же будет применять стилизацию для анимации которую мы сейчас реализуем.

ToolTipComponent.tsx

import React, { ReactElement, useRef, useState } from 'react';
import classes from './ToolTipComponent.module.css';
import { CSSTransition } from 'react-transition-group';

type PropsType = {
  children: ReactElement;
  text: string;
  customClass?: string;
};

const transitionClasses = {
  enter: classes.exampleEnter,
  enterActive: classes.exampleEnterActive,
  exit: classes.exampleExit,
  exitActive: classes.exampleExitActive,
};

const ToolTipComponent: React.FC<PropsType> = ({ children, text, customClass }) => {
  const refSetTimeout = useRef<NodeJS.Timeout>();
  const [showToolTip, setShowToolTip] = useState(false);
  const toolTipClasses = customClass ? `${classes.tooltip} ${customClass}` : `${classes.tooltip}`;


  const onMouseEnterHandler = () => {
    refSetTimeout.current = setTimeout(() => {
      setShowToolTip(true);
    }, 750);
  };

  const onMouseLeaveHandler = () => {
    clearTimeout(refSetTimeout.current);
    setShowToolTip(false);
  };

  return (
    <div className={classes.container} onMouseEnter={onMouseEnterHandler} onMouseLeave={onMouseLeaveHandler}>
      {children}
      <CSSTransition in={showToolTip} timeout={750} classNames={transitionClasses} unmountOnExit>
        <div className={toolTipClasses}>{text}</div>
      </CSSTransition>
    </div>
  );
};

export default ToolTipComponent;

Для стилизации анимации у компонента «CSSTransition» задействуется его 4 внутренних класса:

  • enter — как компонент выглядит на начале анимации появления;

  • enterActive — как компонент выглядит в процессе анимации появления;

  • exit — как компонент выглядит на конце анимации исчезновения;

  • exitActive — как компонент выглядит в процессе анимации исчезновения.

Зададим в css классах для нашей всплывающей подсказки плавное ее появление через свойство «opacity,» и такой же плавный спуск сверху через transform: translateY().

ToolTipComponent.module.css

.container {
  position: relative;
  display: flex;
}

.tooltip {
  position: absolute;
  width: 180px;
  padding: 4px 12px;
  margin-left: calc(100%);
  justify-content: center;
  color: #FFFFFF;
  background-color: #FF8E00;
  border-radius: 12px ;
  text-align: center;
  white-space: pre-line;
  font-weight: 700 ;
  pointer-events: none;
}

.exampleEnter {
  opacity: 0;
  transform:translateY(-100%);
}
.exampleEnterActive {
  opacity: 1;
  transform:translateY(0);
  transition: opacity 350ms, transform 350ms;
}
.exampleExit {
  opacity: 1;
  transform:translateY(0);
}
.exampleExitActive {
  opacity: 0;
  transform:translateY(-100%);
  transition: opacity 350ms, transform 350ms;
}

И теперь финальная версия нашей всплывающей, точнее уже плавно спускающейся сверху и так же плавно уезжающей вниз подсказки на экране пользователя выглядит так.

Поигравшись со стилями класса «.toolTipCustom» в файле App.module.css вы легко настроите внешний вид вашей всплывающей подсказки. А поигравшись со стилями «.exampleEnter», «.exampleEnterActive», «.exampleExit», «.exampleExitActive» в файле ToolTipComponent.module.cssвы легко настроите любую анимацию появления и исчезновения вашей подсказки. Вы можете менять время задержки, время анимации, стороны появления и исчезновения, цвета, размеры, шрифты и пользоваться всей мощью стилизации через css классы, что нам предоставляют современные браузеры.

Вы свободны экспериментировать с кодом приведенным в данной статье и использовать его в своих интересах и проектах.

Код приведенный в статье доступен на GitHub.

P. S. — Статья написана в память о моей классной работе в 2021–2023 году в компании «E‑ngineers», с коллегами которые много чему научили меня за эти два года которые мы программировали вместе!!!

С уважением, alexeyk500.

Комментарии (11)


  1. p07a1330
    01.02.2023 19:31

    Не самый удачный подход, пусть и имеет право на жизнь
    Во первых, в 90% случаев стоит прежде чем рассказывать за кастомные тултипы, рассказать что почти наверняка достаточно использовать пакет/библиотеку с ними
    Во вторых, для отображения/не отображения блока по ховеру хватит и такой конструкции (чуть упрощаю)

    .parent{
      & .tooltip{
          display: none
      }
      &:hover .tooltip{
          display: block
      }
    }

    И как раз вот тут можно аккуратно навесить анимацию, не используя тяжеловесный СSSTransition

    В третьих, кнопка у Вас просит на себя нажать, а ручка приделана к ховеру
    Ну и наконец, на устройствах с сенсорным экраном нужно проверить, будет ли работать код выше - могу ошибиться, но вроде бы нет


    1. alexeyk500 Автор
      01.02.2023 19:43
      +1

      display: none - всего лишь скрывает видимость блока, а по факту его React рендерит в dom.

      Зачем в dom лишние элементы, которые может никогда может и не будут показаны в основной своей массе?

      А если таких элементов много на странице?

      Например - прокручиваемый список из элементов в каждом из котрых есть тултип скрытый display: none, предствьте как у вас dom раздует.

      ИМХО: display: none - это совсем не в философии React


      1. p07a1330
        01.02.2023 20:14

        Если говорить про философию Реакта, то намного более разумно положить где-то поближе к корню ReactModalPortal и пробрасывать коллбек туда, например - используя контексты или MobX. Т.е. те самые порталы

        И уже там отрисовывается через display

        По поводу того, что Реакт рендерит блок при загрузке страницы - да, такое есть. Зато у вас при показе/сокрытии модалки компонент ремаунтится. Поэтому для тех случаев, в которых модалки применяются чаще всего - подходы как минимум сопоставимы в плане перфоманса. Да и условный рендеринг - не совсем бесплатная штука

        А вот читаемость у решения в статье как минимум спорная, имхо


    1. mosx1
      01.02.2023 19:53

      на сенсорном экране hover считается как клик и замирает в положении наведения.


      1. p07a1330
        01.02.2023 20:16

        Как читается ховер я в курсе)

        Тут скорее вопрос про onMouseEnter и onMouseLeave


    1. XeL077
      03.02.2023 16:32

      Через display: none нельзя будет управлять состоянием показа до наведения.
      Напрмер, показывать tooltip если валидация не прошла.


  1. DarthVictor
    01.02.2023 19:38

    Что произойдет с подсказкой, если компонент к которому она показывается, будет внутри контейнера с overflow: hidden?


    1. p07a1330
      01.02.2023 19:41

      Она обрежется на границе контейнера
      Сталкивался с этой проблемой, призодится обходть довольно нетривиальным путем


  1. siandreev
    01.02.2023 19:54
    +3

    Лучше использовать порталы для решения этой проблемы, иначе можно столкнуться с "войной миров z (индексов)" и обрезанием тултипа родительским блоком. А еще лучше взять что-то типа https://floating-ui.com/ , чтобы учесть скролл, невлезание тултипа в нужном месте и тд


  1. AtachiShadow
    01.02.2023 19:55

    Я конечно не фронтендщик и не UI/UX дизайнер, но мне подобная подсказка появляющаяся в рандомном месте сайта очень не нравится. Я привык к подсказкам из под курсора - куда смотришь, там и читаешь. И на мой скромный вкус, лучше такого классического "из_под_курсорного" варианта нет. Поэтому за старание я конечно могу поставить автору + но вот за UX точно -.


  1. strokoff
    03.02.2023 14:07

    Не очень понимаю, зачем вам столько кода ради одного тултипа, так можно быстро выгореть не доходя до релиза. Позвольте, предложу свой вариант, которым пользуюсь , правда я это называю hints

    *[data-hint] {
    	position: relative;
    }
    *[data-hint]:hover:after {
    	visibility: visible;
    	transform: translate(-50%, -100%);
    }
    *[data-hint]:after {
    	visibility: hidden;
    	position: absolute;
    	transition: transform .2s ease;
    	top: -3px;
    	left: 50%;
    	z-index: 2;
    	transform: translate(-50%, 0%);
    	background-color: var(--color-blue-gray-700);
    	color: #fff;
    	content: attr(data-hint);
    	display: inline-block;
    	padding: 3px;
    	border-radius: 3px;
    	font-size: 0.8em;
    	line-height: 0.8em;
    }

    вот и все. пользоваться просто `<span data-hint="вот так могу">подсказка</span>` и сэкономленное время можно потратить на изучение более серьезных подходов в сайтостроении