Naked UI

NakedScrollbar

State-aware scrollbar over Flutter RawScrollbar

Pass the same ScrollController to NakedScrollbar and its scrollable. The wrapper retains RawScrollbar painting, fading, hit testing and thumb dragging. thumbColor, trackColor, trackBorderColor and thickness accept WidgetStateProperty values for hover and drag styling.

thumbVisibility, trackVisibility, interactive, radius, margins and fade durations control presentation. A visible track requires a visible thumb. Do not combine shape and radius; dispose controllers owned by your widget.

Basic implementation

apps/naked_ui_example/lib/api/naked_scrollbar.0.dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

import '../src/example_app.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const ExampleApp(child: Center(child: ScrollbarExample()));
  }
}

class ScrollbarExample extends StatefulWidget {
  const ScrollbarExample({super.key});

  @override
  State<ScrollbarExample> createState() => _ScrollbarExampleState();
}

class _ScrollbarExampleState extends State<ScrollbarExample> {
  final _controller = ScrollController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return PrimaryScrollController(
      controller: _controller,
      child: SizedBox(
        height: 240,
        width: 280,
        child: NakedScrollbar(
          thumbVisibility: true,
          thickness: const WidgetStatePropertyAll(8),
          radius: const Radius.circular(8),
          thumbColor: WidgetStateProperty.resolveWith((states) {
            if (states.contains(WidgetState.dragged)) {
              return Colors.blue.shade700;
            }
            if (states.contains(WidgetState.hovered)) return Colors.blue;
            return Colors.blue.shade200;
          }),
          child: ListView.builder(
            primary: true,
            itemCount: 30,
            itemBuilder: (context, index) => Padding(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
              child: Text('Row $index'),
            ),
          ),
        ),
      ),
    );
  }
}

Source and accessibility

See the package implementation for the complete API. Keep visible labels, focus indication and sufficient contrast in your presentation layer.

View page source on GitHub