Introduction
Popovers are a common UI pattern used to display contextual information or provide quick access to actions. They are often used in dashboards, data tables, or forms. In this tutorial, I'll show you how to create a fully dynamic popover using CSS and JavaScript.
This solution is designed to handle the following challenges:
- Dynamic positioning based on the viewport to prevent overflow.
- Arrow alignment pointing to the triggering element.
- Smooth behavior when scrolling or interacting with the page.
By the end of this tutorial, you will have a reusable and responsive popover implementation.
Step 1: HTML Structure
To start, we assume a basic HTML structure where the popover will be triggered by clicking an SVG icon. Here’s an example of a column title group, such as you might find in a data table:
<div class="dt-column-title-group">
<svg>...</svg>
<span class="dt-column-title">Example Title</span>
</div>```
The **popover** will be dynamically created and injected into the DOM when the user clicks on the `<svg>` icon.
---
## Step 2: Styling the Popover with CSS
To ensure the popover looks clean and professional, we’ll style it using CSS. The following code includes the base styles for the popover and the arrow, along with dynamic adjustments to the arrow’s position (top, bottom, left, or right).
### Popover Base Styles
The base styles define the shape, background, border, and shadow of the popover:
```css
.popover {
position: relative; /* Ensures the arrow can position relative to this */
width: 100%; /* Makes the popover content fit dynamically */
border-radius: 0.375rem; /* Rounded corners for a softer look */
border: 1px solid #E2E8F0; /* Subtle border for contrast */
background-color: #fff; /* White background for clean content display */
box-shadow: 0 0.5em 1em -0.125em #0a0a0a1a, 0 0 0 1px #0a0a0a05; /* Light shadow for depth */
z-index: 1001; /* Ensure it's above other elements */
}
 Popover Arrow Styles
The arrow is a small rotated square positioned along the edge of the popover. By default, the arrow points downward.
.popover:after {
content: "";
background-color: #fff; /* Matches the popover's background */
border-color: #E2E8F0; /* Matches the popover's border */
border-right-width: 1px;
border-bottom-width: 1px;
height: 1rem;
width: 1rem;
position: absolute;
bottom: -0.5rem; /* Positioned below the popover */
left: 50%; /* Centered horizontally */
transform: translateX(-50%) rotate(45deg); /* Creates the arrow shape */
}
Arrow Position Adjustments
Depending on the popover’s position, the arrow’s location needs to change dynamically. The following classes adjust the arrow’s placement:
Arrow on Top
When the popover is positioned below the triggering element, the arrow points upward.
.popover.arrow-top:after {
bottom: auto; /* Remove the default bottom positioning */
top: -0.5rem; /* Position the arrow above the popover */
border-bottom-width: 0; /* Hide the bottom border */
border-top-width: 1px; /* Show the top border */
}
Arrow on the Right
When the popover is aligned to the left of the triggering element, the arrow points to the right.
.popover.arrow-right:after {
top: 50%; /* Vertically centered */
left: 97%; /* Positioned to the right of the popover */
bottom: auto; /* Remove the default bottom positioning */
transform: translateY(-50%) rotate(45deg); /* Keeps the arrow correctly aligned */
border-right-width: 1px; /* Show the right border */
border-left-width: 0; /* Hide the left border */
}
Dynamic Arrow Positioning
With these styles, the arrow can dynamically adjust its position based on the popover’s placement. This creates a seamless and responsive experience for users interacting with your UI.
Step 3: Adding JavaScript for Dynamic Behavior
To make the popover interactive and dynamically positioned, we’ll use JavaScript. This script handles the creation, positioning, and cleanup of the popover as users interact with the page.
Initial Setup
The script begins by selecting all SVG icons that will act as popover triggers. We also maintain a state variable, isPopoverOpen
, to track whether the popover is currently displayed.
document.querySelectorAll('.dt-column-title-group svg').forEach((svgIcon) => {
let isPopoverOpen = false;
svgIcon.addEventListener('click', (event) => {
event.stopPropagation(); // Prevent event propagation to avoid unwanted behavior
let popoverContainer = document.querySelector('.popover-container');
if (isPopoverOpen) {
// If the popover is open, remove it
popoverContainer?.remove();
isPopoverOpen = false;
return;
}
if (!popoverContainer) {
const popoverElement = document.createElement('div');
popoverElement.innerHTML = '<div class="popover">Popover Content</div>';
popoverElement.classList.add('popover-container');
document.body.appendChild(popoverElement);
const popoverHTML = popoverElement.querySelector('.popover');
Popover Positioning Logic
The positionPopover
function calculates the popover’s ideal position based on the trigger's location (svgIcon
) and ensures it fits within the viewport. The arrow's placement is also adjusted dynamically.
const positionPopover = () => {
const targetRect = svgIcon.getBoundingClientRect();
let topPosition = targetRect.top - popoverElement.offsetHeight - 20; // Default: above the element
let leftPosition = targetRect.left + targetRect.width / 2 - popoverElement.offsetWidth / 2;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Reset arrow classes
popoverHTML?.classList.remove('arrow-top', 'arrow-bottom', 'arrow-left', 'arrow-right');
// Adjust if the popover exceeds the top boundary
if (topPosition < 0) {
topPosition = targetRect.bottom + 10; // Place below the element
popoverHTML?.classList.add('arrow-top'); // Arrow on top
} else {
popoverHTML?.classList.add('arrow-bottom'); // Arrow on bottom
}
// Adjust if the popover exceeds the bottom boundary
if (topPosition + popoverElement.offsetHeight > viewportHeight) {
topPosition = targetRect.top - popoverElement.offsetHeight - 10; // Place above the element
popoverHTML?.classList.remove('arrow-bottom');
popoverHTML?.classList.add('arrow-top'); // Arrow on top
}
// Adjust if the popover exceeds the right boundary
if (leftPosition + popoverElement.offsetWidth > viewportWidth) {
leftPosition = targetRect.left - popoverElement.offsetWidth - 20; // Place to the left
topPosition = targetRect.top; // Align with the top of the trigger
popoverHTML?.classList.remove('arrow-bottom', 'arrow-top');
popoverHTML?.classList.add('arrow-right'); // Arrow on the right
}
// Adjust if the popover exceeds the left boundary
if (leftPosition < 0) {
leftPosition = targetRect.right + 20; // Place to the right
topPosition = targetRect.top; // Align with the top of the trigger
popoverHTML?.classList.remove('arrow-bottom', 'arrow-top');
popoverHTML?.classList.add('arrow-left'); // Arrow on the left
}
popoverElement.style.position = 'fixed';
popoverElement.style.top = `${topPosition}px`;
popoverElement.style.left = `${leftPosition}px`;
};
positionPopover(); // Initial positioning
Handling Scroll Events
To keep the popover correctly positioned when the user scrolls, we add a scroll
event listener:
const handleScroll = () => {
if (isPopoverOpen) {
positionPopover();
}
};
window.addEventListener('scroll', handleScroll);
Cleanup: Closing the Popover
The popover should be removed when the user clicks outside of it or when the trigger is no longer visible. This is managed with two event listeners:
document.addEventListener('click', function handleClickOutside(event) {
if (!popoverElement.contains(event.target) && !svgIcon.contains(event.target)) {
popoverElement.remove();
isPopoverOpen = false;
document.removeEventListener('click', handleClickOutside);
window.removeEventListener('scroll', handleScroll);
}
});
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
popoverElement.remove();
isPopoverOpen = false;
observer.disconnect();
window.removeEventListener('scroll', handleScroll);
}
});
}, { threshold: 0.1 });
observer.observe(svgIcon);
Step 4: Testing the Popover
After implementing the HTML, CSS, and JavaScript for the popover, it's important to test its behavior in different scenarios. Follow these steps to ensure your popover works as expected.
1. Basic Functionality
-
Triggering the Popover:
- Click the SVG icon.
- Verify that the popover appears.
- Confirm the popover is positioned correctly based on the viewport and does not overflow.
-
Closing the Popover:
- Click outside the popover.
- Verify that the popover is removed from the DOM.
2. Dynamic Positioning
-
Viewport Boundaries:
- Test with the triggering element placed near the edges of the screen:
- Top Edge: Verify the popover appears below the element with the arrow pointing upward.
- Bottom Edge: Verify the popover appears above the element with the arrow pointing downward.
- Left Edge: Verify the popover appears to the right of the element with the arrow pointing to the left.
- Right Edge: Verify the popover appears to the left of the element with the arrow pointing to the right.
-
Scrolling:
- Open the popover and scroll the page.
- Ensure the popover adjusts its position dynamically and remains aligned with the triggering element.
3. Responsiveness
- Test on different screen sizes (desktop, tablet, and mobile).
- Verify that the popover remains functional and adjusts its position properly on smaller screens.
4. Edge Cases
-
Small Viewports:
- Shrink the browser window to a very small size.
- Verify that the popover adapts to remain within the viewport.
-
Hidden Trigger:
- Scroll until the triggering element (SVG icon) goes out of view.
- Verify that the popover is automatically removed when the trigger is no longer visible.
-
Multiple Popovers:
- Add multiple triggering elements on the page.
- Verify that clicking on one trigger does not interfere with the behavior of others.
5. Final Touches
-
Styling Consistency:
- Ensure the popover design (background, border, arrow) matches the overall theme of your website.
- Check for any visual glitches when transitioning between positions (e.g., top to bottom or left to right).
-
Performance:
- Open the developer tools in your browser.
- Monitor for any errors or warnings in the console.
- Ensure there are no unnecessary re-renders or memory leaks when adding or removing the popover.
Conclusion
Testing the popover ensures a seamless user experience across different scenarios. By addressing edge cases and ensuring responsiveness, your popover implementation will be robust and production-ready.
Let me know how this worked for your project, and feel free to share your insights or customizations!
Top comments (0)