Flutter: SingleChildScrollView Widget

When building Flutter apps, you’ll often find yourself with content that doesn’t fit on the screen. For example, maybe you have a long form or a list of items that stretches beyond the bottom of the screen.

That’s where SingleChildScrollView comes in. It lets you make a single widget scrollable when it overflows the screen. This article will help you understand how it works, how to use its key properties, and how to avoid common layout issues.

What Is SingleChildScrollView?

The SingleChildScrollView widget lets you scroll a single child — like a Column, Row, or any widget that might grow too big. It’s perfect for content that doesn’t fit on the screen and needs to scroll.

It only supports one direct child, so wrap multiple children in a layout widget like Column or Row.

Basic Example

Let’s look at a simple vertical scroll example:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: SingleChildScrollView(
          child: Column(
            children: List.generate(50, (index) => Text('Item ${index + 1}')),
          ),
        ),
      ),
    );
  }
}

This scroll view wraps a Column with many Text widgets. When the total height becomes more than the screen, it scrolls automatically.

Flutter SingleChildScrollView Widget

scrollDirection

The scrollDirection property controls whether the scroll is vertical or horizontal.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: List.generate(
              10,
              (index) => Container(
                width: 100,
                margin: const EdgeInsets.all(8),
                color: Colors.blue,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

By default, the scroll direction is vertical. But in this example, we change it to horizontal so the boxes scroll left and right.

Flutter SingleChildScrollView Widget

Values of Axis enum:

  • Axis.vertical: Scrolls up and down (default)
  • Axis.horizontal: Scrolls left and right

reverse

The reverse property controls the direction of the scroll. When set to true, it flips the direction.

Here’s how it behaves:

  • For vertical scrolls:
    • reverse: false: Scroll from top to bottom.
    • reverse: true: Scroll from bottom to top.
  • For horizontal scrolls:
    • reverse: false: Scroll from left to right (LTR languages).
    • reverse: true: Scroll from right to left.
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: SingleChildScrollView(
            scrollDirection: Axis.vertical,
            reverse: true,
            child: Column(
              children: List.generate(50, (index) => Text('Item ${index + 1}')),
            ),
          ),
        ),
      ),
    );
  }
}

This makes the last item appear at the top, and the list scrolls upward.

Flutter SingleChildScrollView Widget

padding

You can add spacing around the scrollable content using the padding property:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: List.generate(50, (index) => Text('Item ${index + 1}')),
            ),
          ),
        ),
      ),
    );
  }
}

This adds padding inside the scroll view, giving space around its child.

Flutter SingleChildScrollView Widget

physics

The physics property controls how the scrolling behaves. You can customize the scroll effect using different scroll physics:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: SingleChildScrollView(
          child: SingleChildScrollView(
            physics: BouncingScrollPhysics(),
            child: Column(
              children: List.generate(50, (index) => Text('Item ${index + 1}')),
            ),
          ),
        ),
      ),
    );
  }
}

Flutter SingleChildScrollView Widget

The physics property defines the scroll motion and effect. Common values for ScrollPhysics:

  • AlwaysScrollableScrollPhysics(): Always scrollable, even if content is smaller than screen.
  • NeverScrollableScrollPhysics(): Disables scrolling.
  • BouncingScrollPhysics(): Adds bounce effect (iOS-style).
  • ClampingScrollPhysics(): Android-style scroll with no bounce.

Handling Overflow

If you put too many widgets inside a Column without scrolling, you’ll get an overflow error. Wrapping the Column in a SingleChildScrollView prevents that:

Example:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: SingleChildScrollView(
          child: Column(
            children: List.generate(50, (index) => Text('Item ${index + 1}')),
          ),
        ),
      ),
    );
  }
}

This allows the column to scroll instead of overflowing the screen.

Flutter Flexible Widget

Using Expanded or Flexible Inside SingleChildScrollView

Normally, using Expanded or Flexible inside a SingleChildScrollView causes layout errors. That’s because scroll views let their children grow as tall as they want — there’s no fixed size. But Expanded and Flexible only work when their parent gives them a clear height.

To make them work inside a scroll view, you need to wrap them in a widget with a fixed height, like a SizedBox or a Container. This gives Expanded or Flexible the boundaries they need.

Using SizedBox to Set a Fixed Height

Here’s how you can use a SizedBox to give space to an Expanded widget inside a scroll view:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: SingleChildScrollView(
          child: SizedBox(
            height: 400, // Fixed height
            child: Column(
              children: [Expanded(child: Container(color: Colors.blue))],
            ),
          ),
        ),
      ),
    );
  }
}

In this example, the SizedBox tells the Column it has only 400 pixels of height to work with. That lets Expanded divide that space safely.

Using LayoutBuilder and ConstrainedBox

You can also use LayoutBuilder to get the available height dynamically, and then use ConstrainedBox to apply a limit:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ScrollView Widget',
      home: Scaffold(
        appBar: AppBar(title: Text('ScrollView Widget')),
        body: LayoutBuilder(
          builder: (context, constraints) {
            return SingleChildScrollView(
              child: ConstrainedBox(
                constraints: BoxConstraints(maxHeight: constraints.maxHeight),
                child: Column(
                  children: [Expanded(child: Container(color: Colors.green))],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

This example reads the maximum height from the parent and uses it to cap the height of the scrollable area. It’s a bit more advanced, but helpful when you want your layout to be flexible and adaptive.

Flutter Flexible Widget

The Main Idea

If you want to use Expanded or Flexible inside a scroll view:

  • Don’t place them directly inside the scroll view.
  • Wrap them in a widget that gives them a fixed height.
  • Use SizedBox or LayoutBuilder with ConstrainedBox to do this.

This way, Flutter knows how much space to give the widget, and everything works without layout errors.

Conclusion

The SingleChildScrollView widget is very useful when you have content that might overflow the screen. Whether it’s a long list, a form, or a horizontal set of cards — this widget makes your content scroll smoothly.

Remember:

  • Use it to avoid overflow errors.
  • Use scrollDirection, padding, and physics to customize behavior.
  • Be careful with Expanded and Flexible. Wrap them with widgets that give a fixed height like SizedBox or use LayoutBuilder.

Once you understand how SingleChildScrollView works, you’ll be able to build flexible, scrollable UIs with ease.