npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-overlay

Import

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-ui

Truy cập: http://localhost:4500/components/scroll-overlay