Directional List Hover
A list component where a background tile slides in from the exact direction the pointer enters each row and exits in the same direction on leave. Direction detection supports vertical (y), horizontal (x), or all-sides (all) modes.
Code
<div data-directional-hover="" data-type="y" class="directional-list">
<div class="directional-list__info">
<div class="directional-list__col-award">
<p class="direcitonal-list__eyebrow">Award</p>
</div>
<div class="directional-list__col-client">
<p class="direcitonal-list__eyebrow">Client</p>
</div>
<div class="directional-list__col-year">
<p class="direcitonal-list__eyebrow">Year</p>
</div>
</div>
<div class="directional-list__collection">
<div class="directional-list__list">
<a data-directional-hover-item="" href="https://www.flowfest.co.uk/" target="_blank" class="directional-list__item">
<div data-directional-hover-tile="" class="directional-list__hover-tile"></div>
<div class="directional-list__border is--item"></div>
<div class="directional-list__col-award">
<p class="direcitonal-list__p">Site of the Day</p>
</div>
<div class="directional-list__col-client">
<p class="direcitonal-list__p">FlowFest</p>
</div>
<div class="directional-list__col-year">
<p class="direcitonal-list__p">2025</p>
</div>
</a>
<a data-directional-hover-item="" href="https://www.osmo.supply/" target="_blank" class="directional-list__item">
<div data-directional-hover-tile="" class="directional-list__hover-tile"></div>
<div class="directional-list__border is--item"></div>
<div class="directional-list__col-award">
<p class="direcitonal-list__p">Product Honors</p>
</div>
<div class="directional-list__col-client">
<p class="direcitonal-list__p">Osmo</p>
</div>
<div class="directional-list__col-year">
<p class="direcitonal-list__p">2025</p>
</div>
</a>
<a data-directional-hover-item="" href="https://brand.docusign.com/" target="_blank" class="directional-list__item">
<div data-directional-hover-tile="" class="directional-list__hover-tile"></div>
<div class="directional-list__border is--item"></div>
<div class="directional-list__col-award">
<p class="direcitonal-list__p">Site of the Day</p>
</div>
<div class="directional-list__col-client">
<p class="direcitonal-list__p">Docusign Brand</p>
</div>
<div class="directional-list__col-year">
<p class="direcitonal-list__p">2024</p>
</div>
</a>
<a data-directional-hover-item="" href="https://aanstekelijk.nl/" target="_blank" class="directional-list__item">
<div data-directional-hover-tile="" class="directional-list__hover-tile"></div>
<div class="directional-list__border is--item"></div>
<div class="directional-list__col-award">
<p class="direcitonal-list__p">Site of the Day</p>
</div>
<div class="directional-list__col-client">
<p class="direcitonal-list__p">Aanstekelijk</p>
</div>
<div class="directional-list__col-year">
<p class="direcitonal-list__p">2023</p>
</div>
</a>
</div>
</div>
<div class="directional-list__border"></div>
</div>.directional-list {
color: #ffecde;
flex-flow: column;
width: 100%;
max-width: 50em;
display: flex;
position: relative;
}
.directional-list__info {
grid-column-gap: 1em;
grid-row-gap: 1em;
justify-content: space-between;
align-items: center;
width: 100%;
padding-bottom: 1.5em;
padding-left: 1.5em;
padding-right: 1.5em;
display: flex;
position: relative;
}
.direcitonal-list__eyebrow {
color: #c96d4d;
letter-spacing: .1em;
text-transform: uppercase;
margin-bottom: 0;
font-size: .75em;
line-height: 1;
}
.directional-list__item {
grid-column-gap: 1em;
grid-row-gap: 1em;
color: inherit;
justify-content: space-between;
align-items: center;
margin-top: -1px;
padding: 2.25em 1.5em;
text-decoration: none;
display: flex;
position: relative;
overflow: hidden;
}
.directional-list__col-award {
min-width: 30%;
position: relative;
}
.directional-list__col-client {
flex: 1;
position: relative;
}
.directional-list__col-year {
flex: none;
min-width: 3em;
position: relative;
}
.direcitonal-list__p {
margin-bottom: 0;
font-size: 1em;
line-height: 1;
}
.directional-list__border {
z-index: 2;
opacity: .3;
background-color: currentColor;
width: 100%;
height: 1px;
position: absolute;
bottom: 0;
left: 0;
}
.directional-list__border.is--item {
top: 0;
bottom: auto;
}
.directional-list__hover-tile {
background-color: #ab4e2d;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
[data-directional-hover-tile] {
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
transform: translateY(-100%);
will-change: transform;
}function initDirectionalListHover() {
const directionMap = {
top: 'translateY(-100%)',
bottom: 'translateY(100%)',
left: 'translateX(-100%)',
right: 'translateX(100%)'
};
document.querySelectorAll('[data-directional-hover]').forEach(container => {
const type = container.getAttribute('data-type') || 'all';
container.querySelectorAll('[data-directional-hover-item]').forEach(item => {
const tile = item.querySelector('[data-directional-hover-tile]');
if (!tile) return;
item.addEventListener('mouseenter', e => {
const dir = getDirection(e, item, type);
tile.style.transition = 'none';
tile.style.transform = directionMap[dir] || 'translate(0, 0)';
void tile.offsetHeight;
tile.style.transition = '';
tile.style.transform = 'translate(0%, 0%)';
item.setAttribute('data-status', `enter-${dir}`);
});
item.addEventListener('mouseleave', e => {
const dir = getDirection(e, item, type);
item.setAttribute('data-status', `leave-${dir}`);
tile.style.transform = directionMap[dir] || 'translate(0, 0)';
});
});
function getDirection(event, el, type) {
const { left, top, width: w, height: h } = el.getBoundingClientRect();
const x = event.clientX - left;
const y = event.clientY - top;
if (type === 'y') return y < h / 2 ? 'top' : 'bottom';
if (type === 'x') return x < w / 2 ? 'left' : 'right';
const distances = {
top: y,
right: w - x,
bottom: h - y,
left: x
};
return Object.entries(distances).reduce((a, b) => (a[1] < b[1] ? a : b))[0];
}
});
}
// Initialize Directional List Hover
document.addEventListener('DOMContentLoaded', () => {
initDirectionalListHover();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| [data-directional-hover] | attribute | — | The container element. Wraps all hoverable items and scopes the direction detection and event listeners. |
| [data-type] | "y" | "x" | "all" | "all" | Controls which directions are detected. "y" detects top/bottom only, "x" detects left/right only, "all" detects all four sides by finding the nearest edge. |
| [data-directional-hover-item] | attribute | — | Each hoverable row element. mouseenter and mouseleave events are attached here, and a data-status attribute (e.g. "enter-top", "leave-bottom") is applied dynamically for CSS state hooks. |
| [data-directional-hover-tile] | attribute | — | The animated background tile inside each item. On hover enter it snaps to the entry direction off-screen with transition: none, then transitions to translate(0%, 0%). On hover leave it transitions back out in the exit direction. |
| data-status | "enter-top" | "enter-bottom" | "enter-left" | "enter-right" | "leave-top" | "leave-bottom" | "leave-left" | "leave-right" | — | Dynamically set on [data-directional-hover-item] by the script. Use this attribute in CSS to trigger additional state-based styling beyond the tile animation. |
Notes
- •No external dependencies — runs on vanilla JS with CSS transitions only.
- •The void tile.offsetHeight line forces a browser reflow between the instant position snap and the transition, ensuring the enter animation always plays from off-screen.
- •The tile's default CSS transform should match the most common entry direction for your layout. For a vertical list with data-type="y", initializing it at translateY(-100%) means hovering the top half requires no repositioning.
- •Multiple [data-directional-hover] containers on the same page are fully supported — each is scoped independently.
- •The data-status attribute on each item can be used as a CSS hook to animate other child elements in sync with the tile.
Guide
Container
Wrap your hover items in an element with [data-directional-hover]. You can optionally define the direction detection using the [data-type] attribute. Use "y" for vertical detection (top/bottom), "x" for horizontal detection (left/right), or "all" to support all four directions. If no data-type is defined, it defaults to "all".
<div data-directional-hover data-type="y">
...
</div>Item
Each hoverable element should use [data-directional-hover-item]. This is where the hover events are attached, and where the data-status attribute is dynamically applied by JavaScript — for example "enter-top" or "leave-left". You can use this attribute in CSS to drive additional animations beyond the tile.
Tile
Inside each item, the actual animated element should be marked with [data-directional-hover-tile]. This tile will animate in from the correct direction on hover in, and exit in the matching direction on hover out. The transition: none snap trick ensures there is no visible transition when repositioning the tile before the enter animation.