@libs-ui/components-scroll-overlay
v0.2.357-4
Published
> Directive tạo thanh cuộn tùy chỉnh (custom scrollbar) dạng overlay, thay thế thanh cuộn mặc định của trình duyệt với giao diện đồng nhất trên mọi hệ điều hành.
Downloads
4,886
Readme
@libs-ui/components-scroll-overlay
Directive tạo thanh cuộn tùy chỉnh (custom scrollbar) dạng overlay, thay thế thanh cuộn mặc định của trình duyệt với giao diện đồng nhất trên mọi hệ điều hành.
Giới thiệu
LibsUiComponentsScrollOverlayDirective là một Angular standalone directive giúp thay thế thanh cuộn mặc định của trình duyệt bằng một thanh cuộn tùy chỉnh hiển thị dạng overlay — không chiếm diện tích layout. Directive tự động bọc element host trong một div container, inject CSS global một lần duy nhất, và quản lý toàn bộ tương tác kéo thả, hover, resize qua RxJS và requestAnimationFrame throttling. Mỗi instance độc lập màu sắc thông qua CSS custom properties.
Tính năng
- Thanh cuộn tùy chỉnh thay thế native scrollbar, giao diện đồng nhất trên Windows, macOS, Linux
- Hỗ trợ cả scroll dọc (Y) và scroll ngang (X) đồng thời
- Tự động ẩn/hiện khi hover vào container, giữ hiện khi đang kéo thả
- Kéo thả (drag & drop) thumb với xử lý mouse chính xác
- Click vào track để nhảy đến vị trí scroll tương ứng
- Tự động tính lại kích thước thumb khi nội dung thay đổi (interval poll mỗi 1s khi hover + dimension cache)
- Resize cửa sổ tự động cập nhật scrollbar
- Scroll handler throttle qua
requestAnimationFrame— tránh layout thrashing - Màu sắc per-instance qua CSS variables, không ghi đè lẫn nhau
- Phát 5 loại output event:
outScroll,outScrollX,outScrollY,outScrollTop,outScrollBottom - Hỗ trợ truyền element riêng để tính
scrollWidth/scrollHeight(hữu ích khi content element khác scroll element)
Khi nào sử dụng
- Khi cần giao diện thanh cuộn đồng bộ trên các trình duyệt và hệ điều hành khác nhau
- Khi cần thanh cuộn nhỏ gọn, overlay lên nội dung mà không chiếm diện tích layout
- Khi cần tùy biến màu sắc thanh cuộn theo theme của ứng dụng
- Khi cần lắng nghe sự kiện scroll lên đầu hoặc xuống cuối để trigger lazy loading
- Khi muốn ẩn hoàn toàn scrollbar nhưng vẫn cho phép scroll bằng mouse wheel hoặc touch
Cài đặt
npm install @libs-ui/components-scroll-overlayImport
import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
import { IScrollOverlayOptions } from '@libs-ui/components-scroll-overlay';
@Component({
standalone: true,
imports: [LibsUiComponentsScrollOverlayDirective],
})
export class MyComponent {}Ví dụ sử dụng
Ví dụ 1 — Scroll dọc cơ bản
<div
LibsUiComponentsScrollOverlayDirective
class="h-64 w-full overflow-y-auto p-4 border rounded bg-white"
>
@for (item of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; track item) {
<div class="p-3 mb-2 bg-gray-50 rounded">
Item {{ item }}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</div>
}
</div>import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsScrollOverlayDirective],
})
export class MyComponent {}Ví dụ 2 — Scroll ngang với nội dung rộng
<div
LibsUiComponentsScrollOverlayDirective
class="w-full overflow-x-auto border rounded bg-white"
>
<div class="flex gap-4 w-[1200px] p-4">
@for (item of [1, 2, 3, 4, 5, 6, 7, 8]; track item) {
<div class="w-48 h-32 bg-blue-100 rounded flex-shrink-0 flex items-center justify-center">
Card {{ item }}
</div>
}
</div>
</div>Ví dụ 3 — Tùy chỉnh màu sắc và kích thước qua options
<div
LibsUiComponentsScrollOverlayDirective
[options]="scrollOptions"
class="h-80 w-full overflow-auto border rounded bg-white p-4"
>
<div class="w-[900px]">
<p>Nội dung có thể scroll cả dọc lẫn ngang...</p>
@for (i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; track i) {
<div class="p-3 mb-2 bg-gray-50 rounded">Dòng {{ i }}</div>
}
</div>
</div>import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { LibsUiComponentsScrollOverlayDirective, IScrollOverlayOptions } from '@libs-ui/components-scroll-overlay';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsScrollOverlayDirective],
})
export class MyComponent {
protected readonly scrollOptions: IScrollOverlayOptions = {
scrollbarWidth: 8,
scrollbarPadding: 2,
scrollbarColor: '#f1f5f9',
scrollbarHoverColor: '#e2e8f0',
scrollThumbColor: '#94a3b8',
scrollThumbHoverColor: '#64748b',
scrollX: 'scroll',
scrollY: 'scroll',
};
}Ví dụ 4 — Lắng nghe sự kiện scroll để lazy load
<div
LibsUiComponentsScrollOverlayDirective
class="h-96 w-full overflow-y-auto border rounded"
(outScrollBottom)="handlerScrollBottom($event)"
(outScrollTop)="handlerScrollTop($event)"
(outScrollY)="handlerScrollY($event)"
>
@for (item of items(); track item.id) {
<div class="p-4 border-b">{{ item.name }}</div>
}
@if (loading()) {
<div class="p-4 text-center text-gray-400">Đang tải thêm...</div>
}
</div>import { Component, ChangeDetectionStrategy, signal, inject } from '@angular/core';
import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
import { ItemApiService } from './item-api.service';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsScrollOverlayDirective],
})
export class MyListComponent {
private readonly apiService = inject(ItemApiService);
protected items = signal<{ id: string; name: string }[]>([]);
protected loading = signal(false);
protected handlerScrollBottom(event: Event): void {
event.stopPropagation();
if (this.loading()) return;
this.loadMore();
}
protected handlerScrollTop(event: Event): void {
event.stopPropagation();
// xử lý khi scroll lên đầu
}
protected handlerScrollY(event: Event): void {
event.stopPropagation();
// xử lý mỗi lần scroll dọc
}
private loadMore(): void {
this.loading.set(true);
// gọi API tải thêm dữ liệu
}
}Ví dụ 5 — Dynamic content với signal (tự cập nhật scrollbar)
<div class="flex gap-2 mb-3">
<button (click)="handlerAddItem()">+ Thêm item</button>
<button (click)="handlerRemoveItem()">- Xóa item</button>
</div>
<div
LibsUiComponentsScrollOverlayDirective
class="h-64 w-full overflow-y-auto border rounded bg-gray-50 p-3"
>
@for (item of items(); track item) {
<div class="p-3 mb-2 bg-white rounded shadow-sm">{{ item }}</div>
}
</div>import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { LibsUiComponentsScrollOverlayDirective } from '@libs-ui/components-scroll-overlay';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsScrollOverlayDirective],
})
export class DynamicListComponent {
protected items = signal<string[]>([
'Thiết kế giao diện người dùng',
'Viết unit test cho module auth',
'Review Pull Request #124',
'Cập nhật tài liệu API',
]);
protected handlerAddItem(): void {
this.items.update((list) => [...list, `Item mới ${list.length + 1}`]);
// Sau khi thêm item, hover vào vùng scroll để scrollbar tự cập nhật (~1s)
}
protected handlerRemoveItem(): void {
if (this.items().length <= 1) return;
this.items.update((list) => list.slice(0, -1));
}
}Ví dụ 6 — Ẩn scrollbar hoàn toàn nhưng vẫn scroll được
<!-- scrollXOpacity0: true → scrollbar ngang không bao giờ hiện, nhưng vẫn scroll được bằng touch/trackpad -->
<div
LibsUiComponentsScrollOverlayDirective
[options]="hiddenScrollbarOptions"
class="w-full overflow-x-auto"
>
<div class="flex gap-4 w-[2000px] p-4">
@for (item of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; track item) {
<div class="w-40 h-24 bg-indigo-100 rounded flex-shrink-0 flex items-center justify-center">
Card {{ item }}
</div>
}
</div>
</div>import { Component, ChangeDetectionStrategy } from '@angular/core';
import { LibsUiComponentsScrollOverlayDirective, IScrollOverlayOptions } from '@libs-ui/components-scroll-overlay';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsScrollOverlayDirective],
})
export class HiddenScrollbarComponent {
protected readonly hiddenScrollbarOptions: IScrollOverlayOptions = {
scrollX: 'scroll',
scrollY: 'hidden',
scrollXOpacity0: true,
};
}@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [options] | IScrollOverlayOptions | {} | Cấu hình toàn bộ scrollbar: màu sắc, kích thước, chế độ cuộn và các flags | [options]="{ scrollbarWidth: 8, scrollThumbColor: '#94a3b8' }" |
| [classContainer] | string | '' | Class CSS bổ sung vào div container bao ngoài mà directive tạo ra | [classContainer]="'shadow-lg rounded-xl'" |
| [elementScroll] | HTMLElement | Host element | Override element cần scroll thay vì dùng host element mặc định | [elementScroll]="innerScrollRef" |
| [elementCheckScrollX] | HTMLElement | undefined | Element dùng để tính scrollWidth — dùng khi content element khác với scroll element | [elementCheckScrollX]="contentRef" |
| [elementCheckScrollY] | HTMLElement | undefined | Element dùng để tính scrollHeight — dùng khi content element khác với scroll element | [elementCheckScrollY]="contentRef" |
| [debugMode] | boolean | false | Bật chế độ debug: ghi log ra console khi có thay đổi kích thước scrollbar | [debugMode]="true" |
| [ignoreInit] | boolean | false | Nếu true, directive bỏ qua toàn bộ khởi tạo scrollbar — dùng cho lazy init có điều kiện | [ignoreInit]="!isVisible()" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outScroll) | Event | Emit mỗi khi có sự kiện scroll (cả X lẫn Y) | handlerScroll(event: Event): void { event.stopPropagation(); ... } | (outScroll)="handlerScroll($event)" |
| (outScrollBottom) | Event | Emit khi scroll xuống đến cuối nội dung (tolerance 3px) | handlerScrollBottom(event: Event): void { event.stopPropagation(); this.loadMore(); } | (outScrollBottom)="handlerScrollBottom($event)" |
| (outScrollTop) | Event | Emit khi scroll về đầu (scrollTop === 0) | handlerScrollTop(event: Event): void { event.stopPropagation(); ... } | (outScrollTop)="handlerScrollTop($event)" |
| (outScrollX) | Event | Emit khi cuộn ngang (chỉ khi scrollLeft thực sự thay đổi) | handlerScrollX(event: Event): void { event.stopPropagation(); ... } | (outScrollX)="handlerScrollX($event)" |
| (outScrollY) | Event | Emit khi cuộn dọc (chỉ khi scrollTop thực sự thay đổi) | handlerScrollY(event: Event): void { event.stopPropagation(); ... } | (outScrollY)="handlerScrollY($event)" |
Types & Interfaces
import { IScrollOverlayOptions, TYPE_SCROLL_DIRECTION, TYPE_SCROLL_OVERFLOW } from '@libs-ui/components-scroll-overlay';export interface IScrollOverlayOptions {
// Kích thước
scrollbarWidth?: number; // Độ rộng của track scrollbar tính bằng px (default: 10)
scrollbarPadding?: number; // Khoảng cách padding của thumb so với cạnh track (default: 2)
// Màu sắc
scrollbarColor?: string; // Màu nền của track khi không hover (default: transparent)
scrollbarHoverColor?: string; // Màu nền của track khi hover (default: '#CDD0D640')
scrollThumbColor?: string; // Màu của thumb scrollbar (default: '#CDD0D6')
scrollThumbHoverColor?: string; // Màu của thumb khi hover (default: '#9CA2AD')
// Chế độ cuộn
scrollX?: TYPE_SCROLL_OVERFLOW; // 'scroll' | 'hidden' — kiểm soát overflow-x (default: 'scroll')
scrollY?: TYPE_SCROLL_OVERFLOW; // 'scroll' | 'hidden' — kiểm soát overflow-y (default: 'scroll')
// Flags hành vi
scrollXOpacity0?: boolean; // Giữ scrollbar ngang luôn ẩn (opacity 0) dù hover — vẫn scroll được
scrollYOpacity0?: boolean; // Giữ scrollbar dọc luôn ẩn (opacity 0) dù hover — vẫn scroll được
ignoreTransparentScrollBarColorDefault?: boolean; // Không ép native scrollbar thành trong suốt — dùng 'auto' thay vì 'transparent transparent'
}
export type TYPE_SCROLL_DIRECTION = 'X' | 'Y';
export type TYPE_SCROLL_OVERFLOW = 'hidden' | 'scroll';Cấu trúc DOM được tạo ra
Khi directive khởi tạo, nó tự động wrap host element như sau:
<!-- Trước khi áp dụng directive -->
<div class="h-64 overflow-y-auto" LibsUiComponentsScrollOverlayDirective>
<!-- nội dung -->
</div>
<!-- Sau khi directive khởi tạo -->
<div class="libs-ui-scroll-overlay-container relative min-h-0 min-w-0 h-64">
<!-- Host element (đã thêm class libs-ui-scroll-overlay-element) -->
<div class="h-64 overflow-y-auto libs-ui-scroll-overlay-element" style="scrollbar-width: none; overflow-x: scroll; overflow-y: scroll;">
<!-- nội dung -->
</div>
<!-- Track và thumb cho scroll X -->
<div class="scrollbar-track scrollbar-track-X" style="height: 10px; display: none;">
<div class="scrollbar-thumb scrollbar-thumb-X" style="width: 60px; left: 0;"></div>
</div>
<!-- Track và thumb cho scroll Y -->
<div class="scrollbar-track scrollbar-track-Y" style="width: 10px; display: none;">
<div class="scrollbar-thumb scrollbar-thumb-Y" style="height: 60px; top: 0;"></div>
</div>
</div>CSS Custom Properties
Mỗi instance directive tự inject CSS variables lên container div để không ảnh hưởng lẫn nhau:
| CSS Variable | Tương ứng với | Giá trị mặc định |
|---|---|---|
| --libs-ui-sb-track-color | scrollbarColor | transparent |
| --libs-ui-sb-track-hover-color | scrollbarHoverColor | #CDD0D640 |
| --libs-ui-sb-thumb-color | scrollThumbColor | #CDD0D6 |
| --libs-ui-sb-thumb-hover-color | scrollThumbHoverColor | #9CA2AD |
| --libs-ui-sb-padding | scrollbarPadding | 2px |
| --libs-ui-sb-padding-double | scrollbarPadding * 2 | 4px |
Lưu ý quan trọng
⚠️ DOM Wrapping: Directive tự động bọc host element trong một div.libs-ui-scroll-overlay-container. CSS selectors dựa vào cấu trúc cha-con (ví dụ: .parent > div) có thể bị ảnh hưởng. Kiểm tra kỹ layout sau khi áp dụng directive.
⚠️ Class kích thước tự động kế thừa: Directive tự copy một số class kích thước từ host element (w-full, w-screen, h-full, h-screen, h-[Npx], w-[Npx], min-h-*, min-w-*, shrink-0) sang container. Nếu layout bị sai, kiểm tra xem class có được copy đúng không.
⚠️ Dynamic content cần hover: Directive dùng interval(1000) khi hover để poll scrollHeight/scrollWidth và so sánh với cache dimension. Sau khi thêm/xóa nội dung hoặc thay đổi chiều cao element, người dùng cần hover vào vùng scroll để scrollbar cập nhật trong vòng ~1s. Đây là thiết kế chủ ý nhằm tránh dùng MutationObserver({ attributes, subtree }) gây fire 50-200+ lần/giây khi hover.
⚠️ position: relative: Container div được tạo ra luôn có position: relative. Nếu layout cần position: static hoặc layout cụ thể, cần dùng [classContainer] để điều chỉnh.
⚠️ scrollX/scrollY: 'hidden': Khi đặt cả hai thành 'hidden', host element sẽ có class overflow-hidden, ngăn mọi scroll. Chỉ dùng khi thực sự muốn block scroll hoàn toàn.
⚠️ Không dùng với overflow: visible: Directive yêu cầu host element có thể scroll (overflow có giá trị). Nếu element không có nội dung tràn, scrollbar sẽ không hiển thị (ẩn tự động theo display: none).
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/components/scroll-overlay
