GUILD OF GLEKS UIv21.14.0

gog-scroll

Scroll

A drop-in replacement for a native overflow: auto region. Content keeps scrolling natively — wheel, touch, keyboard, focus-into-view all still work — only the browser's own scrollbar chrome is hidden and replaced with a themeable, draggable overlay thumb.

Overview

Import the component and give it a height (or width).

typescript
import { ScrollComponent } from '@guildofgleks/ui';

@Component({
  // ...
  imports: [ScrollComponent],
})

Row 1

Row 2

Row 3

Row 4

Row 5

Row 6

Row 7

Row 8

Row 9

Row 10

Row 11

Row 12

Row 13

Row 14

Row 15

Row 16

Row 17

Row 18

Row 19

Row 20

Row 21

Row 22

Row 23

Row 24

Row 25

Row 26

Row 27

Row 28

Row 29

Row 30

<gog-scroll class="scroll-box" ariaLabel="Example list">
  @for (item of items; track item) {
    <p class="scroll-row">{{ item }}</p>
  }
</gog-scroll>

Examples

Axis

vertical (default), horizontal, or both. Native scrolling on a disabled axis is left completely alone.

Column 1Column 2Column 3Column 4Column 5Column 6Column 7Column 8Column 9Column 10Column 11Column 12Column 13Column 14Column 15Column 16Column 17Column 18Column 19Column 20
<gog-scroll axis="horizontal" class="scroll-box" ariaLabel="Example columns">
  <div class="scroll-row-flex">
    @for (item of items; track item) {
      <span class="scroll-chip">{{ item }}</span>
    }
  </div>
</gog-scroll>

Horizontal wheel 21.9.0

Hover the first row and turn the wheel: the page moves. A vertical wheel over a region with nothing to scroll vertically belongs to the browser, and this component draws its thumb over native scrolling rather than replacing it, so that is the default and always was. [horizontalWheel]="true" is the opt-in exception — the second row is the same markup with that one input.

What it does not do is the part worth reading. Scroll the second row to its right-hand end and keep turning: the page starts moving again. The translation only happens while there is room left in the direction of the turn, so a region can never swallow the wheel and leave the page feeling stuck — which would be a worse bug than the one this fixes. It also keeps out of the way of anything that already works: a trackpad's sideways swipe and Shift+wheel carry a horizontal delta the browser applies correctly, Ctrl+wheel is pinch-zoom, and a region that can still scroll vertically is left alone — so axis="both" keeps scrolling down while there is down to go.

Off by default, because it changes what an existing region does with a gesture it currently passes on. GOG_CONFIG.scroll.horizontalWheel turns it on app-wide.

off — a vertical wheel scrolls the page (the default)

Column 1Column 2Column 3Column 4Column 5Column 6Column 7Column 8Column 9Column 10Column 11Column 12Column 13Column 14Column 15Column 16Column 17Column 18Column 19Column 20

horizontalWheel — the same turn scrolls the row

Column 1Column 2Column 3Column 4Column 5Column 6Column 7Column 8Column 9Column 10Column 11Column 12Column 13Column 14Column 15Column 16Column 17Column 18Column 19Column 20
<!-- Both rows are the same content on the same axis. The only difference is the input, which is
     the point: hover each and turn the wheel. -->
<p class="wheel-label">off — a vertical wheel scrolls the page (the default)</p>
<gog-scroll axis="horizontal" class="scroll-box" ariaLabel="Columns, wheel off">
  <div class="scroll-row-flex">
    @for (item of items; track item) {
      <span class="scroll-chip">{{ item }}</span>
    }
  </div>
</gog-scroll>

<p class="wheel-label">horizontalWheel — the same turn scrolls the row</p>
<gog-scroll
  axis="horizontal"
  [horizontalWheel]="true"
  class="scroll-box"
  ariaLabel="Columns, wheel translated"
>
  <div class="scroll-row-flex">
    @for (item of items; track item) {
      <span class="scroll-chip">{{ item }}</span>
    }
  </div>
</gog-scroll>

Size and auto-hide

size="thin" shrinks the track/thumb; autoHide="false" keeps the thumb always visible instead of fading out after hideDelay ms. [showTrack]="false"21.3.1 removes the visual affordance altogether — scrolling itself (wheel, touch, keyboard) is unaffected either way, which is what makes it safe for a region whose scrolling is driven for the user, the way gog-tabs drives its header row. Settable app-wide via GOG_CONFIG.scroll.showTrack.

Row 1

Row 2

Row 3

Row 4

Row 5

Row 6

Row 7

Row 8

Row 9

Row 10

Row 11

Row 12

Row 13

Row 14

Row 15

Row 16

Row 17

Row 18

Row 19

Row 20

Row 21

Row 22

Row 23

Row 24

Row 25

Row 26

Row 27

Row 28

Row 29

Row 30

<div class="controls">
  <gog-button size="sm" (gogClick)="size.set(size() === 'normal' ? 'thin' : 'normal')">
    Size: {{ size() }}
  </gog-button>
  <gog-button size="sm" (gogClick)="autoHide.set(!autoHide())">
    Auto-hide: {{ autoHide() ? 'on' : 'off' }}
  </gog-button>
</div>

<gog-scroll [size]="size()" [autoHide]="autoHide()" class="scroll-box" ariaLabel="Example list">
  @for (item of items; track item) {
    <p class="scroll-row">{{ item }}</p>
  }
</gog-scroll>

Reach events

gogReachStart / gogReachEnd fire once per crossing; gogScroll emits live metrics on every scroll/resize.

Row 1

Row 2

Row 3

Row 4

Row 5

Row 6

Row 7

Row 8

Row 9

Row 10

Row 11

Row 12

Row 13

Row 14

Row 15

Row 16

Row 17

Row 18

Row 19

Row 20

Row 21

Row 22

Row 23

Row 24

Row 25

Row 26

Row 27

Row 28

Row 29

Row 30

Scroll to the edges to see gogReachStart / gogReachEnd fire.

<gog-scroll
  class="scroll-box"
  ariaLabel="Example list"
  [reachThreshold]="8"
  (gogReachStart)="reachState.set('At the top')"
  (gogReachEnd)="reachState.set('At the bottom')"
  (gogScroll)="onScroll($event)"
>
  @for (item of items; track item) {
    <p class="scroll-row">{{ item }}</p>
  }
</gog-scroll>

<p class="status">
  {{ reachState() }}
  @if (lastMetrics(); as metrics) {
    — scrollTop: {{ metrics.scrollTop | number: '1.0-0' }}
  }
</p>

Scroll methods

Grab the instance with viewChild and call scrollToTop / scrollToBottom / scrollToLeft / scrollToRight / scrollTo directly.

Row 1

Row 2

Row 3

Row 4

Row 5

Row 6

Row 7

Row 8

Row 9

Row 10

Row 11

Row 12

Row 13

Row 14

Row 15

Row 16

Row 17

Row 18

Row 19

Row 20

Row 21

Row 22

Row 23

Row 24

Row 25

Row 26

Row 27

Row 28

Row 29

Row 30

<div class="controls">
  <gog-button size="sm" (gogClick)="scroller()?.scrollToTop()">To top</gog-button>
  <gog-button size="sm" (gogClick)="scroller()?.scrollToBottom()">To bottom</gog-button>
</div>

<!-- The reference is `scrollRegion`, not `scroller`: a template reference variable shadows a
     class member of the same name, so `scroller()` would resolve to the component instance
     rather than to the viewChild signal, and the call would not compile. -->
<gog-scroll #scrollRegion class="scroll-box" ariaLabel="Example list">
  @for (item of items; track item) {
    <p class="scroll-row">{{ item }}</p>
  }
</gog-scroll>

Global defaults with GOG_CONFIG

size, autoHide, hideDelay and overscrollBehavior default to undefined so they can fall through to an app-wide (or subtree-wide) default set once via provideGogConfig(...), instead of repeating the same inputs on every instance. An instance's own input, when set, always wins.

This example has no template — it is configuration, not markup.

API Reference

Inputs

NameTypeDefaultDescription
axis'vertical' | 'horizontal' | 'both''vertical'Which axes get an overlay thumb. Native scrolling on the other axis is unaffected.
size'normal' | 'thin' | undefinedundefinedUnset, falls back to GOG_CONFIG.scroll.size, then to 'normal'.
autoHideboolean | undefinedundefinedFades the thumb out after hideDelay ms of inactivity; false keeps it always visible. Unset, falls back to GOG_CONFIG.scroll.autoHide, then to true.
hideDelaynumber | undefinedundefinedUnset, falls back to GOG_CONFIG.scroll.hideDelay, then to 800.
reachThresholdnumber0Pixel distance from an edge that still counts as "reached" for gogReachStart/gogReachEnd.
focusablebooleantrueRenders the viewport as its own tab stop (tabindex="0", role="region"). Turn off when nesting inside a component that already owns focus/keyboard handling.
ariaLabelstring''Accessible name for the viewport when focusable is true and there is no visible label.
horizontalWheel21.9.0boolean | undefinedundefinedTurns a vertical wheel into horizontal scrolling on a region that has nothing to scroll vertically. Only acts while there is room left in that direction — at the content’s end the event is left alone and the page takes it — and never touches a horizontal delta (trackpad, Shift+wheel) or a pinch-zoom. Unset, falls back to GOG_CONFIG.scroll.horizontalWheel, then to false.
showTrack21.3.1boolean | undefinedundefinedWhether the overlay thumb and track render at all. Scrolling itself is unaffected either way — this is purely the visual affordance. Unset, falls back to GOG_CONFIG.scroll.showTrack, then to true.
overscrollBehavior'auto' | 'contain' | 'none' | undefinedundefinedWhat happens when a scroll gesture reaches this instance's edge. Unset, falls back to GOG_CONFIG.scroll.overscrollBehavior, then to 'auto' — chains to the next scrollable ancestor.

Outputs

NamePayloadDescription
gogScrollGogScrollMetricsEmits scrollTop/scrollLeft/scrollHeight/scrollWidth/clientHeight/clientWidth on every scroll/resize.
gogReachStart'vertical' | 'horizontal'Fires once when that axis crosses back within reachThreshold of its start edge.
gogReachEnd'vertical' | 'horizontal'Fires once when that axis crosses within reachThreshold of its end edge.

Methods

NameDescription
scrollTo(options: ScrollToOptions)Forwards to the viewport's native Element.scrollTo.
scrollToTop(behavior = 'smooth')Scrolls to the top.
scrollToBottom(behavior = 'smooth')Scrolls to the bottom.
scrollToLeft(behavior = 'smooth')Scrolls to the left edge.
scrollToRight(behavior = 'smooth')Scrolls to the right edge.

Global Configuration

Set any of these once with provideGogConfig() instead of repeating them on every instance — an instance's own input still wins when it sets one itself. See Global Configuration for the full reference.

  • GOG_CONFIG.scroll.size
  • GOG_CONFIG.scroll.autoHide
  • GOG_CONFIG.scroll.hideDelay
  • GOG_CONFIG.scroll.overscrollBehavior
  • GOG_CONFIG.scroll.showTrack
  • GOG_CONFIG.scroll.horizontalWheel — turns a vertical wheel into horizontal scrolling on a region with nothing to scroll vertically. Off by default, and it never takes a gesture the browser already handles — a horizontal delta, a pinch-zoom, or a turn at the content’s end.

Styling Tokens

Every CSS custom property the scrollbar overlay paints with. Override any of them — on a single instance, a subtree, or a theme — to restyle it. See the Theming guide for the full token-layering model, or the Theme Generator to tweak these live.

TokenDescription
--gog-scroll-track-bg / -track-radiusTrack background and corner radius.
--gog-scroll-thumb-bg / -thumb-hover-bg / -thumb-active-bgDraggable thumb color, per interaction state.
--gog-scroll-thumb-radius / -thumb-insetThumb shape and inset from the track edges.
--gog-scroll-corner-bgBackground of the corner square where two tracks meet.
--gog-scroll-fade-durationAuto-hide fade animation timing.
--gog-scroll-focus-ring / -focus-ring-widthKeyboard focus ring on the viewport.
--gog-scroll-{normal|thin}-track-width / -thumb-min-sizeTrack width and minimum thumb length, per size step.