Resource Hover Tooltip
Usage
theme="ios" You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant="light" You can turn auto dark mode on based on system settings.
Usage
clickToCreate={true}
// click to create is on dragToCreate={true}
// drag to create is on dragToMove={true}
// reordering is on dragToResize={true}
// resizing is on eventDelete={true}
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale={localeEn} Selecting the locale loads the localized settings and the translated UI text.
Resource Hover Tooltip
Displaying information in a tooltip can come in handy when space is tight but information matters. When hovering over a resource cell, the onResourceHoverIn and onResourceHoverOut lifecycle events are triggered, which can be used to show/hide rich tooltips. Information and actions shown here won't clutter the main view, keep the timeline clean while still giving quick access to content that helps.
Do you want to learn more about customizing the resource header? Take a look at this example →
Demo

Code
View code on GitHub
import { Button, Eventcalendar, formatDate, Popup, setOptions, Toast } from '@mobiscroll/react';
import { useCallback, useMemo, useRef, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
function App() {
const [resourceAvatar, setResourceAvatar] = useState('');
const [resourceName, setResourceName] = useState('');
const [resourceCost, setResourceCost] = useState('');
const [resourceDate, setResourceDate] = useState('');
const [resourceTotal, setResourceTotal] = useState('');
const [currentResource, setCurrentResource] = useState(null);
const [isTooltipOpen, setTooltipOpen] = useState(false);
const [isToastOpen, setToastOpen] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const hoveredResourceElmRef = useRef(null);
const mySelectedDate = useRef(new Date());
const calRef = useRef(null);
const popupRef = useRef(null);
const openTimer = useRef(null);
const closeTimer = useRef(null);
const myResources = useMemo(
() => [
{
id: 'res1',
name: 'Adam Miller',
color: '#F39C12',
profession: 'Mason',
avatar: 'https://img.mobiscroll.com/demos/m1.png',
cost: '15',
},
{
id: 'res2',
name: 'Emily Carter',
color: '#76e083',
profession: 'Electrician',
avatar: 'https://img.mobiscroll.com/demos/f1.png',
cost: '20',
},
{
id: 'res3',
name: 'James Brown',
color: '#b13f49',
profession: 'Carpenter',
avatar: 'https://img.mobiscroll.com/demos/m2.png',
cost: '18',
},
{
id: 'res4',
name: 'Daniel Wilson',
color: '#e25dd2',
profession: 'Welder',
avatar: 'https://img.mobiscroll.com/demos/m3.png',
cost: '22',
},
{
id: 'res5',
name: 'Benjamin Harris',
color: '#7056ff',
profession: 'Plumber',
avatar: 'https://img.mobiscroll.com/demos/m4.png',
cost: '20',
},
{
id: 'res6',
name: 'Olivia Anderson',
color: '#56aaff',
profession: 'Concrete Finisher',
avatar: 'https://img.mobiscroll.com/demos/f2.png',
cost: '15',
},
{
id: 'res7',
name: 'Emma Thompson',
color: '#84852f',
profession: 'Steelworker',
avatar: 'https://img.mobiscroll.com/demos/f3.png',
cost: '18',
},
{
id: 'res8',
name: 'Natalie Roberts',
color: '#ff6e56',
profession: 'Painter',
avatar: 'https://img.mobiscroll.com/demos/f4.png',
cost: '25',
},
],
[],
);
const myEvents = useMemo(
() => [
{
start: '2026-10-25T12:00',
end: '2026-10-25T15:00',
title: 'Repoint Brick Facade',
resource: 'res1',
},
{
start: '2026-10-25T09:00',
end: '2026-10-25T12:00',
title: 'Install Custom Wood Trim',
resource: 'res3',
},
{
start: '2026-10-25T14:00',
end: '2026-10-25T18:00',
title: 'Repair Steel Stair Treads',
resource: 'res4',
},
{
start: '2026-10-25T10:00',
end: '2026-10-25T13:00',
title: 'Pour and Finish Driveway Slab',
resource: 'res6',
},
{
start: '2026-10-25T11:00',
end: '2026-10-25T16:00',
title: 'Paint Interior Drywall',
resource: 'res8',
},
{
start: '2026-10-26T08:00',
end: '2026-10-26T11:00',
title: 'Block Wall Construction',
resource: 'res1',
},
{
start: '2026-10-26T14:00',
end: '2026-10-26T16:00',
title: 'Task 2',
resource: 'Paver Installation',
},
{
start: '2026-10-26T12:00',
end: '2026-10-26T17:00',
title: 'Install ceiling fan',
resource: 'res2',
},
{
start: '2026-10-26T10:00',
end: '2026-10-26T14:00',
title: 'Roof Beam Replacement',
resource: 'res3',
},
{
start: '2026-10-26T07:00',
end: '2026-10-26T12:00',
title: 'Custom Metalworks Creation',
resource: 'res4',
},
{
start: '2026-10-26T14:00',
end: '2026-10-26T17:00',
title: 'Pipe Welding',
resource: 'res4',
},
{
start: '2026-10-10T08:00',
end: '2026-10-11T20:00',
title: 'Leak Detection & Repair',
resource: 'res5',
},
{
start: '2026-10-26T13:00',
end: '2026-10-26T17:00',
title: 'Faucet & Sink Fitting',
resource: 'res5',
},
{
start: '2026-10-26T18:00',
end: '2026-10-26T20:00',
title: 'Drainage System Setup',
resource: 'res5',
},
{
start: '2026-10-26T09:00',
end: '2026-10-26T13:00',
title: 'Surface Polishing',
resource: 'res6',
},
{
start: '2026-10-26T08:00',
end: '2026-10-26T10:00',
title: 'Structural Steel Inspections',
resource: 'res7',
},
{
start: '2026-10-26T13:00',
end: '2026-10-26T16:00',
title: 'Metal Structure Assembly',
resource: 'res7',
},
{
start: '2026-10-26T17:00',
end: '2026-10-26T19:00',
title: 'Heavy Steel Beam Placement',
resource: 'res7',
},
{
start: '2026-10-26T09:00',
end: '2026-10-26T12:00',
title: 'Exterior House Painting',
resource: 'res8',
},
{
start: '2026-10-26T15:00',
end: '2026-10-26T18:00',
title: 'Deck Staining & Sealing',
resource: 'res8',
},
{
start: '2026-10-27T12:00',
end: '2026-10-27T15:00',
title: 'Troubleshoot Faulty Breaker',
resource: 'res2',
},
{
start: '2026-10-27T10:00',
end: '2026-10-27T13:00',
title: 'Frame Interior Partitions',
resource: 'res3',
},
{
start: '2026-10-27T16:00',
end: '2026-10-27T20:00',
title: 'Weld Structural Beam Connections',
resource: 'res4',
},
{
start: '2026-10-27T12:00',
end: '2026-10-27T16:00',
title: 'Apply Smooth Trowel Finish to Basement Floor',
resource: 'res6',
},
],
[],
);
const myView = useMemo(
() => ({
timeline: {
type: 'day',
startTime: '07:00',
endTime: '22:00',
},
}),
[],
);
const getTotalHoursForResource = useCallback(
(resourceId) =>
calRef.current
.getEvents()
.filter((e) => e.resource === resourceId)
.reduce((sum, e) => {
const start = new Date(e.start);
const end = new Date(e.end);
const hours = (end - start) / (1000 * 60 * 60);
return sum + hours;
}, 0),
[],
);
// Handles both Mobiscroll hover and native mouse events
const openTooltip = useCallback(
(resource, target) => {
clearTimeout(closeTimer.current);
clearTimeout(openTimer.current);
openTimer.current = setTimeout(() => {
const totalHours = getTotalHoursForResource(resource.id);
setCurrentResource(resource);
hoveredResourceElmRef.current = target;
setResourceAvatar(resource.avatar);
setResourceName(resource.name);
setResourceCost('$' + resource.cost);
setResourceDate(formatDate('D DDD MMM YYYY', mySelectedDate.current));
setResourceTotal(totalHours + 'h, $' + totalHours * resource.cost);
target.classList.add('mds-resource-info-hover');
popupRef.current.position();
setTooltipOpen(true);
openTimer.current = null;
}, 100);
},
[mySelectedDate, getTotalHoursForResource],
);
const closeTooltip = useCallback(() => {
clearTimeout(openTimer.current);
clearTimeout(closeTimer.current);
closeTimer.current = setTimeout(() => {
setTooltipOpen(false);
closeTimer.current = null;
}, 200);
}, []);
// Mobiscroll resource hover events
const handleResourceHoverIn = useCallback(
(args) => {
openTooltip(args.resource, args.target);
},
[openTooltip],
);
const handleResourceHoverOut = useCallback(
(args) => {
args.target.classList.remove('mds-resource-info-hover');
closeTooltip();
},
[closeTooltip],
);
const handlePageChange = useCallback((args) => {
mySelectedDate.current = args.firstDay;
}, []);
const handleTooltipPosition = useCallback((args, inst) => {
const popupElm = args.target.querySelector('.mbsc-popup');
const rect = hoveredResourceElmRef.current.getBoundingClientRect();
popupElm.style.top = rect.top - 10 + 'px';
if (inst.s.rtl) {
popupElm.style.right = window.innerWidth - rect.left + 10 + 'px';
} else {
popupElm.style.left = rect.right + 10 + 'px';
}
return false; // Prevent default positioning
}, []);
// Native mouse events for the popup
const handlePopupMouseEnter = useCallback(() => {
clearTimeout(closeTimer.current);
}, []);
const handlePopupMouseLeave = useCallback(() => {
closeTooltip();
}, [closeTooltip]);
const customResource = useCallback(
(res) => (
<div className="mbsc-flex">
<img className="mds-resource-info-avatar" src={res.avatar} alt="Avatar" />
<div className="mds-resource-info-details">
<div className="mds-resource-info-title">{res.name}</div>
<div className="mds-resource-info-profession">{res.profession}</div>
</div>
</div>
),
[],
);
const handlePay = () => {
setTooltipOpen(false);
setToastMessage(currentResource.name + ' paid');
setToastOpen(true);
};
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
const handleTooltipClose = useCallback(() => {
setTooltipOpen(false);
}, []);
return (
<>
<Eventcalendar
clickToCreate={ }
dragToCreate={ }
dragToMove={ }
dragToResize={ }
eventDelete={ }
ref={calRef}
data={myEvents}
resources={myResources}
view={myView}
renderResource={customResource}
onResourceHoverIn={handleResourceHoverIn}
onResourceHoverOut={handleResourceHoverOut}
onPageChange={handlePageChange}
/>
<Popup
ref={popupRef}
display="anchored"
showOverlay={false}
touchUi={false}
width={280}
isOpen={isTooltipOpen}
onPosition={handleTooltipPosition}
onClose={handleTooltipClose}
>
<div onMouseEnter={handlePopupMouseEnter} onMouseLeave={handlePopupMouseLeave}>
<div className="mbsc-flex mbsc-align-items-center mds-resource-info-header">
<img className="mds-resource-info-avatar" src={resourceAvatar} alt="Avatar" />
<div className="mds-resource-info-name mbsc-flex-1-0">{resourceName}</div>
<Button color="success" variant="outline" className="mds-resource-info-pay mbsc-button-xs" onClick={handlePay}>
Pay
</Button>
</div>
<div className="mds-resource-info-cont">
<div>
<span className="mbsc-txt-muted">Hourly rate </span>
<span className="mds-resource-info-detail">{resourceCost}</span>
</div>
<div>
<span className="mbsc-txt-muted">{resourceDate}</span>
<span className="mds-resource-info-detail">{resourceTotal}</span>
</div>
</div>
</div>
</Popup>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</>
);
}
export default App;
import {
Button,
Eventcalendar,
formatDate,
MbscCalendarEvent,
MbscEventcalendarView,
MbscPageChangeEvent,
MbscPopupPositionEvent,
MbscResource,
MbscResourceHoverEvent,
Popup,
setOptions,
Toast ,
} from '@mobiscroll/react';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const App: FC = () => {
const [resourceAvatar, setResourceAvatar] = useState<string>('');
const [resourceName, setResourceName] = useState<string>('');
const [resourceCost, setResourceCost] = useState<string>('');
const [resourceDate, setResourceDate] = useState<string>('');
const [resourceTotal, setResourceTotal] = useState<string>('');
const [currentResource, setCurrentResource] = useState<MbscResource>();
const [isTooltipOpen, setTooltipOpen] = useState<boolean>(false);
const [isToastOpen, setToastOpen] = useState<boolean>(false);
const [toastMessage, setToastMessage] = useState<string>('');
const hoveredResourceElmRef = useRef<HTMLElement>(null);
const mySelectedDate = useRef(new Date());
const calInst = useRef<Eventcalendar>(null);
const popupInst = useRef<Popup>(null);
const openTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
const closeTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
const myResources = useMemo<MbscResource[]>(
() => [
{
id: 'res1',
name: 'Adam Miller',
color: '#F39C12',
profession: 'Mason',
avatar: 'https://img.mobiscroll.com/demos/m1.png',
cost: '15',
},
{
id: 'res2',
name: 'Emily Carter',
color: '#76e083',
profession: 'Electrician',
avatar: 'https://img.mobiscroll.com/demos/f1.png',
cost: '20',
},
{
id: 'res3',
name: 'James Brown',
color: '#b13f49',
profession: 'Carpenter',
avatar: 'https://img.mobiscroll.com/demos/m2.png',
cost: '18',
},
{
id: 'res4',
name: 'Daniel Wilson',
color: '#e25dd2',
profession: 'Welder',
avatar: 'https://img.mobiscroll.com/demos/m3.png',
cost: '22',
},
{
id: 'res5',
name: 'Benjamin Harris',
color: '#7056ff',
profession: 'Plumber',
avatar: 'https://img.mobiscroll.com/demos/m4.png',
cost: '20',
},
{
id: 'res6',
name: 'Olivia Anderson',
color: '#56aaff',
profession: 'Concrete Finisher',
avatar: 'https://img.mobiscroll.com/demos/f2.png',
cost: '15',
},
{
id: 'res7',
name: 'Emma Thompson',
color: '#84852f',
profession: 'Steelworker',
avatar: 'https://img.mobiscroll.com/demos/f3.png',
cost: '18',
},
{
id: 'res8',
name: 'Natalie Roberts',
color: '#ff6e56',
profession: 'Painter',
avatar: 'https://img.mobiscroll.com/demos/f4.png',
cost: '25',
},
],
[],
);
const myEvents = useMemo<MbscCalendarEvent[]>(
() => [
{
start: '2026-10-25T12:00',
end: '2026-10-25T15:00',
title: 'Repoint Brick Facade',
resource: 'res1',
},
{
start: '2026-10-25T09:00',
end: '2026-10-25T12:00',
title: 'Install Custom Wood Trim',
resource: 'res3',
},
{
start: '2026-10-25T14:00',
end: '2026-10-25T18:00',
title: 'Repair Steel Stair Treads',
resource: 'res4',
},
{
start: '2026-10-25T10:00',
end: '2026-10-25T13:00',
title: 'Pour and Finish Driveway Slab',
resource: 'res6',
},
{
start: '2026-10-25T11:00',
end: '2026-10-25T16:00',
title: 'Paint Interior Drywall',
resource: 'res8',
},
{
start: '2026-10-26T08:00',
end: '2026-10-26T11:00',
title: 'Block Wall Construction',
resource: 'res1',
},
{
start: '2026-10-26T14:00',
end: '2026-10-26T16:00',
title: 'Task 2',
resource: 'Paver Installation',
},
{
start: '2026-10-26T12:00',
end: '2026-10-26T17:00',
title: 'Install ceiling fan',
resource: 'res2',
},
{
start: '2026-10-26T10:00',
end: '2026-10-26T14:00',
title: 'Roof Beam Replacement',
resource: 'res3',
},
{
start: '2026-10-26T07:00',
end: '2026-10-26T12:00',
title: 'Custom Metalworks Creation',
resource: 'res4',
},
{
start: '2026-10-26T14:00',
end: '2026-10-26T17:00',
title: 'Pipe Welding',
resource: 'res4',
},
{
start: '2026-10-10T08:00',
end: '2026-10-11T20:00',
title: 'Leak Detection & Repair',
resource: 'res5',
},
{
start: '2026-10-26T13:00',
end: '2026-10-26T17:00',
title: 'Faucet & Sink Fitting',
resource: 'res5',
},
{
start: '2026-10-26T18:00',
end: '2026-10-26T20:00',
title: 'Drainage System Setup',
resource: 'res5',
},
{
start: '2026-10-26T09:00',
end: '2026-10-26T13:00',
title: 'Surface Polishing',
resource: 'res6',
},
{
start: '2026-10-26T08:00',
end: '2026-10-26T10:00',
title: 'Structural Steel Inspections',
resource: 'res7',
},
{
start: '2026-10-26T13:00',
end: '2026-10-26T16:00',
title: 'Metal Structure Assembly',
resource: 'res7',
},
{
start: '2026-10-26T17:00',
end: '2026-10-26T19:00',
title: 'Heavy Steel Beam Placement',
resource: 'res7',
},
{
start: '2026-10-26T09:00',
end: '2026-10-26T12:00',
title: 'Exterior House Painting',
resource: 'res8',
},
{
start: '2026-10-26T15:00',
end: '2026-10-26T18:00',
title: 'Deck Staining & Sealing',
resource: 'res8',
},
{
start: '2026-10-27T12:00',
end: '2026-10-27T15:00',
title: 'Troubleshoot Faulty Breaker',
resource: 'res2',
},
{
start: '2026-10-27T10:00',
end: '2026-10-27T13:00',
title: 'Frame Interior Partitions',
resource: 'res3',
},
{
start: '2026-10-27T16:00',
end: '2026-10-27T20:00',
title: 'Weld Structural Beam Connections',
resource: 'res4',
},
{
start: '2026-10-27T12:00',
end: '2026-10-27T16:00',
title: 'Apply Smooth Trowel Finish to Basement Floor',
resource: 'res6',
},
],
[],
);
const myView = useMemo<MbscEventcalendarView>(
() => ({
timeline: {
type: 'day',
startTime: '07:00',
endTime: '22:00',
},
}),
[],
);
const getTotalHoursForResource = useCallback(
(resourceId: string | number) =>
calInst
.current!.getEvents()
.filter((e: MbscCalendarEvent) => e.resource === resourceId)
.reduce((sum: number, e: MbscCalendarEvent) => {
const start = new Date(e.start as Date);
const end = new Date(e.end as Date);
const hours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
return sum + hours;
}, 0),
[],
);
// Handles both Mobiscroll hover and native mouse events
const openTooltip = useCallback(
(resource: MbscResource, target: HTMLElement) => {
clearTimeout(closeTimer.current!);
clearTimeout(openTimer.current!);
openTimer.current = setTimeout(() => {
const totalHours = getTotalHoursForResource(resource.id);
setCurrentResource(resource);
hoveredResourceElmRef.current = target;
setResourceAvatar(resource.avatar);
setResourceName(resource.name!);
setResourceCost('$' + resource.cost);
setResourceDate(formatDate('D DDD MMM YYYY', mySelectedDate.current));
setResourceTotal(totalHours + 'h, $' + totalHours * resource.cost);
popupInst.current!.position();
setTooltipOpen(true);
openTimer.current = undefined;
}, 100);
},
[mySelectedDate, getTotalHoursForResource],
);
const closeTooltip = useCallback(() => {
clearTimeout(openTimer.current);
clearTimeout(closeTimer.current);
closeTimer.current = setTimeout(() => {
setTooltipOpen(false);
closeTimer.current = undefined;
}, 200);
}, []);
// Mobiscroll resource hover events
const handleResourceHoverIn = useCallback(
(args: MbscResourceHoverEvent) => {
openTooltip(args.resource, args.target);
},
[openTooltip],
);
const handleResourceHoverOut = useCallback(
(args: MbscResourceHoverEvent) => {
args.target.classList.remove('mds-resource-info-hover');
closeTooltip();
},
[closeTooltip],
);
const handlePageChange = useCallback((args: MbscPageChangeEvent) => {
mySelectedDate.current = args.firstDay;
}, []);
const handleTooltipPosition = useCallback((args: MbscPopupPositionEvent, inst: Popup) => {
const popupElm = args.target.querySelector('.mbsc-popup') as HTMLElement;
const rect = hoveredResourceElmRef.current!.getBoundingClientRect();
popupElm.style.top = rect.top - 10 + 'px';
if (inst.s.rtl) {
popupElm.style.right = window.innerWidth - rect.left + 10 + 'px';
} else {
popupElm.style.left = rect.right + 10 + 'px';
}
return false; // Prevent default positioning
}, []);
// Native mouse events for the popup
const handlePopupMouseEnter = useCallback(() => {
clearTimeout(closeTimer.current);
}, []);
const handlePopupMouseLeave = useCallback(() => {
closeTooltip();
}, [closeTooltip]);
const customResource = useCallback(
(res: MbscResource) => (
<div className="mbsc-flex">
<img className="mds-resource-info-avatar" src={res.avatar} alt="Avatar" />
<div className="mds-resource-info-details">
<div className="mds-resource-info-title">{res.name}</div>
<div className="mds-resource-info-profession">{res.profession}</div>
</div>
</div>
),
[],
);
const handlePay = () => {
setTooltipOpen(false);
setToastMessage(currentResource!.name + ' paid');
setToastOpen(true);
};
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
const handleTooltipClose = useCallback(() => {
setTooltipOpen(false);
}, []);
return (
<>
<Eventcalendar
clickToCreate={ }
dragToCreate={ }
dragToMove={ }
dragToResize={ }
eventDelete={ }
ref={calInst}
data={myEvents}
resources={myResources}
view={myView}
renderResource={customResource}
onResourceHoverIn={handleResourceHoverIn}
onResourceHoverOut={handleResourceHoverOut}
onPageChange={handlePageChange}
/>
<Popup
ref={popupInst}
display="anchored"
showOverlay={false}
touchUi={false}
width={280}
isOpen={isTooltipOpen}
onPosition={handleTooltipPosition}
onClose={handleTooltipClose}
>
<div onMouseEnter={handlePopupMouseEnter} onMouseLeave={handlePopupMouseLeave}>
<div className="mbsc-flex mbsc-align-items-center mds-resource-info-header">
<img className="mds-resource-info-avatar" src={resourceAvatar} alt="Avatar" />
<div className="mds-resource-info-name mbsc-flex-1-0">{resourceName}</div>
<Button color="success" variant="outline" className="mds-resource-info-pay mbsc-button-xs" onClick={handlePay}>
Pay
</Button>
</div>
<div className="mds-resource-info-cont">
<div>
<span className="mbsc-txt-muted">Hourly rate </span>
<span className="mds-resource-info-detail">{resourceCost}</span>
</div>
<div>
<span className="mbsc-txt-muted">{resourceDate}</span>
<span className="mds-resource-info-detail">{resourceTotal}</span>
</div>
</div>
</div>
</Popup>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</>
);
};
export default App;
.mds-resource-info-avatar {
width: 40px;
height: 40px;
}
.mds-resource-info-details {
margin: 0 8px;
}
.mds-resource-info-title {
line-height: 24px;
}
.mds-resource-info-profession {
font-size: 12px;
opacity: 0.5;
line-height: 18px;
}
.mds-resource-info-hover.mbsc-timeline-resource {
text-decoration: underline;
}
.mds-resource-info-header {
height: 35px;
}
.mds-resource-info-name {
font-size: 18px;
padding: 16px 10px;
}
.mds-resource-info-pay.mbsc-button {
margin: 0;
line-height: 24px;
padding: 0 10px;
will-change: opacity;
}
.mds-resource-info-pay .mbsc-button-txt {
font-size: 12px;
}
.mds-resource-info-cont {
font-size: 14px;
line-height: 22px;
padding-top: 23px;
}
.mbsc-ltr .mds-resource-info-detail {
float: right;
}
.mbsc-rtl .mds-resource-info-detail {
float: left;
}