Skip to content

LayrzScrollbar

Kenny Mochizuki Escalona edited this page Aug 21, 2026 · 3 revisions

LayrzScrollbar

A Material-free scrollbar built on RawScrollbar, providing visible feedback for scrollable content. Automatically installed by default in LayrzApp on all vertical scrollables. Thumb always visible and rounded; track hidden until hover.

Metadata
Mirrors: Scrollbar (Flutter SDK, Material-dependent)
Phase: M5 (Layout, navigation, feedback)
Domain: Scrolling
Primitive: RawScrollbar from package:flutter/widgets.dart
Status: Confirmed scope (D37).


Overview

LayrzScrollbar is a Material-free scrollbar component designed to provide visual feedback on scrollable content. Unlike Flutter's Scrollbar (which is Material-coupled), LayrzScrollbar uses only RawScrollbar and basic container primitives.

Design Principles

  • Always visible thumb: The scroll thumb is always rendered, rounded, and easy to grab (minimum 48px tall)
  • Hidden track until hover: The track is invisible unless the mouse hovers over the scrollbar area
  • Material-free: Uses RawScrollbar from widgets.dart, not the Material Scrollbar
  • Installed by default: LayrzApp installs LayrzScrollBehavior (which uses this scrollbar) by default on all vertical scrollables
  • Vertical only: Horizontal scrollables and touch platforms are exempt

Visibility Behavior

Default (LayrzApp)

When you use LayrzApp without an explicit scrollBehavior parameter, LayrzScrollBehavior is installed automatically. This means:

  • All vertical scrollables (ListView, SingleChildScrollView, etc.) show a LayrzScrollbar
  • Thumb is always visible (rounded, ~8px wide)
  • Track appears on hover
  • Horizontal scrollables do NOT get scrollbars
  • Touch platforms (iOS, Android) do NOT get scrollbars (native scrollbar precedence)

This is a visible change from apps that previously had no scrollbars. Consuming apps accustomed to no scrollbar feedback will now see them.

Opting Out

To disable LayrzScrollbar on specific scrollables or app-wide, pass an explicit scrollBehavior:

App-wide opt-out:

LayrzApp(
  // ... other properties
  scrollBehavior: ScrollBehavior(),
  // This disables LayrzScrollBehavior and all scrollbars
)

Per-widget opt-out:

ScrollConfiguration(
  behavior: ScrollBehavior(),
  child: ListView(
    // This ListView will not show a scrollbar
    children: [...],
  ),
)

Styling

Scrollbars use theme tokens:

  • Thumb color: LayrzTokens.colors.fg2 (secondary foreground)
  • Thumb radius: LayrzTokens.radius.r1 (4 logical pixels)
  • Thumb minimum height: 48px
  • Track color: Transparent (invisible until hover)
  • Track hover color: LayrzTokens.colors.sf3 (nested surface tint)

API Structure

LayrzScrollBehavior

Extends ScrollBehavior to apply LayrzScrollbar to all vertical scrollables.

class LayrzScrollBehavior extends ScrollBehavior {
  /// Creates a scroll behavior with LayrzScrollbar applied to vertical scrollables.
  const LayrzScrollBehavior();

  @override
  Widget buildScrollbar(
    BuildContext context,
    Widget child,
    ScrollableDetails details,
  ) {
    /// Only apply scrollbar to vertical scrollables on desktop platforms.
    /// Touch and horizontal scrollables use native feedback.
    if (details.direction != Axis.vertical) {
      return child;
    }

    switch (details.controller.position.platform) {
      case TargetPlatform.android:
      case TargetPlatform.iOS:
        return child; // Native scrollbar feedback
      default:
        return LayrzScrollbar(
          controller: details.controller,
          child: child,
        );
    }
  }
}

LayrzScrollbar Widget

class LayrzScrollbar extends StatelessWidget {
  /// The scroll controller that drives the thumb position.
  final ScrollController controller;

  /// The scrollable child widget.
  final Widget child;

  /// Creates a [LayrzScrollbar] around [child].
  const LayrzScrollbar({
    super.key,
    required this.controller,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return RawScrollbar(
      controller: controller,
      thickness: 8.0,
      radius: const Radius.circular(4.0),
      thumbColor: context.theme.tokens.colors.fg2,
      trackColor: Colors.transparent,
      trackBorderColor: Colors.transparent,
      trackVisibility: false,
      trackRadius: Radius.zero,
      thumbVisibility: true,
      minThumbLength: 48.0,
      notificationPredicate: _defaultScrollNotificationPredicate,
      isAlwaysShown: false,
      showTrackOnHover: true,
      child: child,
    );
  }
}

Usage

Basic Usage (Default with LayrzApp)

No configuration needed; scrollbars appear automatically:

void main() {
  runApp(
    LayrzApp(
      // ... other properties
      // LayrzScrollBehavior is installed by default
      home: MyHome(),
    ),
  );
}

class MyHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LayrzLayout(
      // ... other properties
      body: ListView(
        children: [...], // This ListView will show a LayrzScrollbar
      ),
    );
  }
}

Wrapping a Scrollable Directly

LayrzScrollbar(
  controller: _scrollController,
  child: ListView(
    controller: _scrollController,
    children: [...],
  ),
)

Custom ScrollBehavior per Screen

class MyScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScrollConfiguration(
      behavior: LayrzScrollBehavior(), // Explicit but redundant (default in LayrzApp)
      child: ListView(
        children: [...],
      ),
    );
  }
}

Disabling Scrollbars on a Specific Widget

class NoScrollbarSection extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScrollConfiguration(
      behavior: ScrollBehavior(), // Plain behavior, no scrollbar
      child: SingleChildScrollView(
        child: MyContent(),
      ),
    );
  }
}

Accessibility

  • Keyboard: Scrollbars are not keyboard-interactive; use arrow keys or Page Up/Down on the scrollable itself
  • Screen readers: Scrollbars are not announced (native scrolling feedback is available via accessibility APIs)
  • Visual contrast: Thumb color (fg2) meets WCAG AA minimum contrast against surfaces

Platform-Specific Behavior

Platform Behavior
Desktop (Linux, Windows, macOS) LayrzScrollbar visible on vertical scrollables
Mobile (iOS, Android) Native scroll feedback takes precedence; no LayrzScrollbar
Web (Wasm) LayrzScrollbar visible on vertical scrollables

Migration from Material Scrollbar

If you were using Flutter's Material Scrollbar:

Before (Material):

Scrollbar(
  controller: _scrollController,
  child: ListView(/* ... */),
)

After (layrz_ui, via LayrzApp default):

// No code change required; scrollbars appear automatically in LayrzApp
LayrzApp(
  home: ListView(/* ... */),
)

If you need explicit control:

LayrzScrollbar(
  controller: _scrollController,
  child: ListView(/* ... */),
)

Related


Last updated: 2026-08-19
Status: Implementation underway (M5)

Clone this wiki locally