Hướng dẫn tạo ứng dụng cuộn ngang bằng HTML, CSS và JavaScript
Chào mọi người,
Đầu năm đi làm lại. Chúc mọi người một năm mới nhiều sức khoẻ, vạn sự như ý!
Hôm nay, mình xin chia sẻ lại một tiện ích (widget) rất cool để sử dụng cho các web/blog cá nhân hay doanh nghiệp đều cũng sẽ thấy rất ổn và nhìn vào rất chuyên nghiệp. Nào, chúng ta cùng bắt tay vào làm việc thôi!
Tạo các phần tử HTML
Trong đoạn mã HTML này, chúng ta có một phần tử <div>
có class là "container" chứa toàn bộ nội dung. Bên trong đó, chúng ta có một phần tử <div>
khác có class là "top-bar" chứa các thông tin về các sự kiện sắp diễn ra. Trong phần này, chúng ta thấy một tiêu đề gọi là "Upcoming events", và hai nút button đại diện cho việc cuộn ngang để xem các sự kiện trước hoặc sau.
Dưới "top-bar", chúng ta có một phần tử <div>
khác có id là "events". Trong phần này, chúng ta có một danh sách các sự kiện (được biểu diễn dưới dạng các phần tử <a>
). Mỗi sự kiện bao gồm một hình ảnh, ngày diễn ra sự kiện và loại sự kiện, cũng như một mô tả vắn tắt về sự kiện đó.
Mỗi sự kiện cũng có một số thông tin khác như số bài viết mới và số vé đã được mua được hiển thị bên dưới mô tả sự kiện.
Cuối cùng, cấu trúc của mỗi sự kiện bao gồm hình ảnh đại diện, ngày và tháng diễn ra sự kiện, loại sự kiện và mô tả sự kiện, tạo nên một giao diện thú vị để hiển thị thông tin về các sự kiện sắp diễn ra.
<div class="container">
<div class="top-bar">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z" />
<path d="M16 3v4" />
<path d="M8 3v4" />
<path d="M4 11h16" />
<path d="M7 14h.013" />
<path d="M10.01 14h.005" />
<path d="M13.01 14h.005" />
<path d="M16.015 14h.005" />
<path d="M13.015 17h.005" />
<path d="M7.01 17h.005" />
<path d="M10.01 17h.005" />
</svg>
<h2>
Upcoming events
</h2>
<button type="button" disabled id="action-button--previous" class="action-button--horizontal-scroll">
<svg width="16" height="16" fill="currentColor" focusable="false" viewBox="0 0 24 24">
<path
d="M12.771 7.115a.829.829 0 0 0-1.2 0L3 15.686l1.2 1.2 7.971-7.971 7.972 7.971 1.2-1.2-8.572-8.571Z">
</path>
</svg>
</button>
<button type="button" id="action-button--next" class="action-button--horizontal-scroll">
<svg width="16" height="16" fill="currentColor" focusable="false" viewBox="0 0 24 24">
<path
d="M12.771 7.115a.829.829 0 0 0-1.2 0L3 15.686l1.2 1.2 7.971-7.971 7.972 7.971 1.2-1.2-8.572-8.571Z">
</path>
</svg>
</button>
</div>
<div id="events">
<a href="#the-weeknd" class="event">
<div class="event__image">
<img src="https://www.urbanstage.cz/wp-content/uploads/2020/03/weeknd.jpg" alt="The Weeknd">
<div class="event__indicator event__date">
08 <div class="event__date__month">
Feb
</div>
</div>
<div class="event__indicator event__type">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 5l0 2" />
<path d="M15 11l0 2" />
<path d="M15 17l0 2" />
<path
d="M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-3a2 2 0 0 0 0 -4v-3a2 2 0 0 1 2 -2" />
</svg>
</div>
</div>
<div class="event-description">
<h2>
The Weeknd - After Hours Tour
</h2>
<div class="bottom-stats">
<div class="bottom-stat">
<div class="circle circle--red"></div>
2 new posts
</div>
<div class="bottom-stat">
<div class="circle circle--green"></div>
5 tickets
</div>
</div>
</div>
</a>
<a href="#the-weeknd" class="event">
<div class="event__image">
<img src="https://media.architecturaldigest.com/photos/641b2b8252ae61ead67e92d9/16:9/w_2560%2Cc_limit/GettyImages-1474485122.jpg"
alt="The Weeknd">
<div class="event__indicator event__date">
16 <div class="event__date__month">
Feb
</div>
</div>
<div class="event__indicator event__type">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 5l0 2" />
<path d="M15 11l0 2" />
<path d="M15 17l0 2" />
<path
d="M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-3a2 2 0 0 0 0 -4v-3a2 2 0 0 1 2 -2" />
</svg>
</div>
</div>
<div class="event-description">
<h2>
Taylor Swift - Eras Tour
</h2>
<div class="bottom-stats">
<div class="bottom-stat">
<div class="circle circle--red"></div>
4 new posts
</div>
<div class="bottom-stat">
<div class="circle circle--green"></div>
10 tickets
</div>
</div>
</div>
</a>
<a href="#the-weeknd" class="event">
<div class="event__image">
<img src="https://fox2now.com/wp-content/uploads/sites/14/2021/09/GettyImages-451833751.jpg?w=1280"
alt="The Weeknd">
<div class="event__indicator event__date">
20 <div class="event__date__month">
Feb
</div>
</div>
<div class="event__indicator event__type">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 5l0 2" />
<path d="M15 11l0 2" />
<path d="M15 17l0 2" />
<path
d="M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-3a2 2 0 0 0 0 -4v-3a2 2 0 0 1 2 -2" />
</svg>
</div>
</div>
<div class="event-description">
<h2>
Imagine Dragons
</h2>
<div class="bottom-stats">
<div class="bottom-stat">
<div class="circle circle--red"></div>
2 new posts
</div>
<div class="bottom-stat">
<div class="circle circle--green"></div>
4 tickets purchased
</div>
</div>
</div>
</a>
<a href="#the-weeknd" class="event">
<div class="event__image">
<img src="https://i.dailymail.co.uk/i/pix/2012/10/05/article-2212936-155E4783000005DC-817_1024x615_large.jpg"
alt="The Weeknd">
<div class="event__indicator event__date">
28 <div class="event__date__month">
March
</div>
</div>
<div class="event__indicator event__type">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M15 5l0 2" />
<path d="M15 11l0 2" />
<path d="M15 17l0 2" />
<path
d="M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-3a2 2 0 0 0 0 -4v-3a2 2 0 0 1 2 -2" />
</svg>
</div>
</div>
<div class="event-description">
<h2>
U2
</h2>
<div class="bottom-stats">
<div class="bottom-stat">
<div class="circle circle--red"></div>
2 new posts
</div>
<div class="bottom-stat">
<div class="circle circle--green"></div>
3 tickets purchased
</div>
</div>
</div>
</a>
</div>
</div>
Thêm các lớp CSS
Dùng đoạn mã css bên dưới để định hình cho nội dung HTML trên được rõ ràng hơn.
.top-bar {
display: flex;
gap: 16px;
align-items: center;
width: 100%;
flex-direction: row;
color: var(--headline-color);
}
.top-bar h2 {
font-size: 1.2rem;
font-weight: 700;
line-height: 1.5rem;
}
.top-bar svg {
color: var(--headline-color);
}
.action-buttons {
position: relative;
display: inline-flex;
flex: 0 0 auto;
gap: 8px;
scroll-snap-align: start;
width: 100%;
flex-wrap: wrap;
}
.action-button {
color: #163300;
border-radius: 16px;
height: 32px;
gap: 4px;
cursor: pointer;
display: flex;
padding: 0 12px;
font-weight: 600;
transition: all 0.2s ease-in-out;
white-space: nowrap;
align-items: center;
text-decoration: none;
background: var(--secondary);
}
.action-button--primary:active {
background: var(--accent-active);
}
.action-button:active {
background: var(--secondary-active);
}
.action-button--horizontal-scroll {
border-radius: 50%;
cursor: pointer;
border: none;
outline: none;
width: 32px;
height: 32px;
background: var(--accent);
}
.action-button--horizontal-scroll:hover {
background: var(--accent-hover);
}
.action-button--horizontal-scroll:active {
background: var(--accent-active);
}
.action-button--horizontal-scroll:disabled {
cursor: not-allowed;
background: var(--secondary);
}
.action-button--horizontal-scroll:disabled:hover {
background: var(--secondary);
}
.action-button--horizontal-scroll:disabled:active {
background: var(--secondary);
}
#action-button--previous {
margin-left: auto;
transform: rotate(-90deg);
}
#action-button--next {
transform: rotate(90deg);
}
.action-button:hover {
background: var(--secondary-hover);
}
.action-button--primary {
background: var(--accent);
}
.action-button--primary:hover {
background: var(--accent-hover);
}
.action-button--primary:active {
background: var(--accent-active);
}
#events {
display: flex;
gap: 1.5rem;
padding: 1rem 0px;
width: 100%;
flex-direction: row;
overflow: auto;
}
.event {
appearance: none;
text-decoration: none;
position: relative;
transition: all 0.2s ease-in-out;
min-width: 320px;
cursor: pointer;
flex-direction: column;
justify-content: space-between;
scroll-snap-align: start;
overflow: hidden;
color: rgba(29, 32, 59, 1);
}
.event__image {
position: relative;
}
.event__indicator {
position: absolute;
padding: 0.5rem;
min-width: 3.5rem;
min-height: 3.5rem;
display: flex;
flex-direction: column;
justify-content: center;
background: var(--white);
align-items: center;
border-radius: 0.5rem;
}
.event__date {
top: 0.5rem;
left: 0.5rem;
font-size: 1.35rem;
}
.event__date__month {
font-size: 0.6rem;
text-transform: uppercase;
font-weight: bold;
color: var(--event-headline);
}
.event__type {
top: 0.5rem;
right: 0.5rem;
}
.event__type svg {
color: var(--event-text);
height: 2rem;
width: 2rem;
}
.event-description {
color: var(--event-text);
padding: 0.5rem 0.75rem;
}
.event img {
width: 100%;
height: 250px;
border-radius: 1rem;
object-fit: cover;
object-position: top;
}
.event h2 {
font-size: 1.375rem;
letter-spacing: normal;
overflow-wrap: normal;
white-space: nowrap;
font-weight: 700;
line-height: 1.25rem;
color: var(--event-headline);
overflow: hidden;
text-overflow: ellipsis;
}
.bottom-stats {
display: flex;
justify-content: space-between;
gap: 1rem;
width: 100%;
flex-direction: row;
color: var(--headline-color);
}
.bottom-stat {
display: flex;
gap: 0.5rem;
align-items: center;
flex-direction: row;
}
.container {
max-width: 800px;
width: 100%;
min-width: 300px;
margin: 120px auto;
}
.circle {
height: 0.5rem;
width: 0.5rem;
border-radius: 50%;
}
.circle--green {
background: var(--success);
}
.circle--red {
background: var(--error);
}
Thêm Javascript
const events = document.getElementById("events");
let isDown = false;
let startX;
let startY;
let scrollLeft;
let scrollTop;
events.addEventListener("mousedown", (e) => {
isDown = true;
startX = e.pageX - events.offsetLeft;
startY = e.pageY - events.offsetTop;
scrollLeft = events.scrollLeft;
scrollTop = events.scrollTop;
events.style.cursor = "grabbing";
});
events.addEventListener("mouseleave", () => {
isDown = false;
events.style.cursor = "grab";
});
events.addEventListener("mouseup", () => {
isDown = false;
events.style.cursor = "grab";
});
document.addEventListener("mousemove", (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - events.offsetLeft;
const y = e.pageY - events.offsetTop;
const walkX = (x - startX) * 1;
const walkY = (y - startY) * 1;
events.scrollLeft = scrollLeft - walkX;
events.scrollTop = scrollTop - walkY;
});
const scrollLeftButton = document.getElementById("action-button--previous");
const scrollRightButton = document.getElementById("action-button--next");
scrollLeftButton.addEventListener("click", () => {
events.scrollBy({
top: 0,
left: -200,
behavior: "smooth",
});
});
scrollRightButton.addEventListener("click", () => {
events.scrollBy({
top: 0,
left: 200,
behavior: "smooth",
});
});
events.addEventListener("scroll", (e) => {
const position = events.scrollLeft;
if (position === 0) {
scrollLeftButton.disabled = true;
} else {
scrollLeftButton.disabled = false;
}
if (Math.round(position) === events.scrollWidth - events.clientWidth) {
scrollRightButton.disabled = true;
} else {
scrollRightButton.disabled = false;
}
});
Mã JavaScript trên là một đoạn mã điều khiển sự kiện scroll và drag trên một phần tử HTML mang ID "events".
Đầu tiên, mã này áp dụng một số biến để lưu trữ thông tin về trạng thái hiện tại của sự kiện kéo thả (mousedown
), bao gồm vị trí chuột khi bắt đầu kéo thả (startX và startY) và vị trí scroll khi bắt đầu kéo thả (scrollLeft
và scrollTop
).
Sau đó, nó gán các event listener để theo dõi hành vi của người dùng. Khi người dùng nhấn chuột vào phần tử "events" (mousedown
), chương trình sẽ lưu trữ vị trí ban đầu của chuột và vị trí scroll. Khi rời khỏi phần tử "events" (mouseleave
), hoặc thả chuột ra (mouseup
), trạng thái của sự kiện kéo thả được cập nhật.
Khi di chuyển chuột trên tài liệu (mousemove
) và nếu trạng thái kéo thả là đúng (isDown = true
), chương trình sẽ tính toán sự thay đổi vị trí chuột và cập nhật vị trí scroll của phần tử "events" tương ứng.
Ngoài ra, mã cũng xử lý sự kiện khi người dùng click vào các nút scroll trái (scrollLeftButton
) và scroll phải (scrollRightButton
). Khi phần tử "events" được di chuyển, trạng thái của các nút scroll cũng được cập nhật để có thể bật/tắt chức năng scroll tương ứng.
Sau khi thêm đầy đủ các thành phần thì khi chạy thử trang web để xem, chúng ta sẽ có được kết quả như sau:
Đến đây là bạn đã code hoàn thành xong một tiện ích để sử dụng cho một trang web. Chúc các bạn thành công!
All rights reserved