Striking border effects in Flutter
Tags: flutter, dart, tutorial, appdevelopment
Published: 2020-07-05 08:32:57

![A collection of special borders with gradients in Flutter](https://dev-to-uploads.s3.amazonaws.com/i/jsmufeq3umrefi90ugsh.gif)

This article is about borders. Not only the boring default one, but also the fun ones that make the containing widget stand out.

# The goal

Apart from the basic usage I want you to learn how to draw special borders with gradients and gaps like in the above animation.

# A simple border

But let's start with something simple: how do we draw a single-colored border around a widget? 

```dart
Container(
  padding: const EdgeInsets.all(8),
  decoration: BoxDecoration(
    border: Border.all(color: Colors.orangeAccent, width: 4)
  ),
  child: Text("Surrounded by a border", style: TextStyle(fontSize: 32),),
);
```

We wrap a container around the widget (in this case the Text widget) and use a `BoxDecoration` to define border width and color. In fact, using the `Border` constructor instead of `Border.all`, we can even control each of the four sides independently.

![Simple text surrounded by border](https://dev-to-uploads.s3.amazonaws.com/i/7a0311s1k36sf1e2d0iq.png)

# Stroke border

When you think of border around text, you might rather think about a stroke that encloses every character instead of a rectangle defined by the outer Container with a border. For this purpose, we can use the foreground property of the `TextStyle` class.

```dart
Stack(
  children: [
    Text(
      'Surrounded by a border',
      style: TextStyle(
        fontSize: 32,
        foreground: Paint()
          ..style = PaintingStyle.stroke
          ..strokeWidth = 4
          ..color = Colors.orangeAccent,
      ),
    ),
    Text(
      'Surrounded by a border',
      style: TextStyle(
        fontSize: 32,
        color: Colors.redAccent,
      ),
    ),
  ]
);
```

If we used only the first Text widget, in the `Stack` we would only have the stroke. Instead, we also want the fill. That's why I chose a `Stack` widget that paints the border in the background and the fill in the foreground. Just stacking two Text widgets with different font sizes on top of each other will not work because the text is scaled from the center.

![Simple text widget with stroke border in Flutter](https://dev-to-uploads.s3.amazonaws.com/i/p6bv944wt94am467sbnl.png)

# Gradient border

Okay, let's take the next step: instead of just having a single-colored border around a widget, we proceed to draw a gradient around it. This will get a little bit more complicated, but no worries, I will go through it step by step.
Let's start by implementing a `CustomPainter`, the one that actually draws the border.
But first, let's think a moment what this class should do:

* In order to draw a border with a gradient, we need at least two pieces of information: the Gradient itself (containing information like the colors and how they are drawn) and the stroke width
* We draw an (inner) rectangle around the widget that we want to have bordered. It has to be an inner rectangle because we don't want to exceed the size of the widget we want to enclose

```dart
class GradientPainter extends CustomPainter {
  GradientPainter({this.gradient, this.strokeWidth});

  final Gradient gradient;
  final double strokeWidth;
  final Paint paintObject = Paint();

  @override
  void paint(Canvas canvas, Size size) {
    Rect innerRect = Rect.fromLTRB(strokeWidth, strokeWidth, size.width - strokeWidth, size.height - strokeWidth);
    Rect outerRect = Offset.zero & size;

    paintObject.shader = gradient.createShader(outerRect);
    Path borderPath = _calculateBorderPath(outerRect, innerRect);
    canvas.drawPath(borderPath, paintObject);
  }

  Path _calculateBorderPath(Rect outerRect, Rect innerRect) {
    Path outerRectPath = Path()..addRect(outerRect);
    Path innerRectPath = Path()..addRect(innerRect);
    return Path.combine(PathOperation.difference, outerRectPath, innerRectPath);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
```

So what we do is making use of a shortcut. Instead of drawing four rectangles (the four sides of our border), we add two paths: the outer rectangle that has the same size as the widget and the inner rectangle that has the same size but subtracted by the given strokeWidth. Then we use `PathOperation.difference` to calculate the difference. The difference between a bigger and a smaller rectangle is a stroke around the smaller one.
To make the gradient work as well, we need to add a shader to the `paintObject`. We use the `createShader` method for that and provide the `outerRect` as an argument to make the gradient reach the outer edges.

Now in order to be able to use that `GradientPainter`, we have to create an enclosing widget that takes a child widget (e. g. a Text) and then draws our `Gradient` around it.

```dart
class GradientBorderButtonContainer extends StatelessWidget {
  GradientBorderButtonContainer({
    @required gradient,
    @required this.child,
    this.strokeWidth = 4,
  }) : this.painter = GradientPainter(
      gradient: gradient, strokeWidth: strokeWidth
  );

  final GradientPainter painter;
  final Widget child;
  final VoidCallback onPressed;
  final double strokeWidth;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
        painter: painter, 
        child: child
    );
  }
}
```

![Gradient border around text in Flutter](https://dev-to-uploads.s3.amazonaws.com/i/crfji9rsltwqkfi8w2ep.png)

## Rounded edges

Now we want the border to be round instead of having the hard rectangle corners.

```dart
class GradientPainter extends CustomPainter {
  GradientPainter({this.gradient, this.strokeWidth, this.borderRadius});

  final Gradient gradient;
  final double strokeWidth;
  final double borderRadius;
  final Paint paintObject = Paint();

  @override
  void paint(Canvas canvas, Size size) {
    Rect innerRect = Rect.fromLTRB(strokeWidth, strokeWidth, size.width - strokeWidth, size.height - strokeWidth);
    RRect innerRoundedRect = RRect.fromRectAndRadius(innerRect, Radius.circular(borderRadius));

    Rect outerRect = Offset.zero & size;
    RRect outerRoundedRect = RRect.fromRectAndRadius(outerRect, Radius.circular(borderRadius));

    paintObject.shader = gradient.createShader(outerRect);
    Path borderPath = _calculateBorderPath(outerRoundedRect, innerRoundedRect);
    canvas.drawPath(borderPath, paintObject);
  }

  Path _calculateBorderPath(RRect outerRRect, RRect innerRRect) {
    Path outerRectPath = Path()..addRRect(outerRRect);
    Path innerRectPath = Path()..addRRect(innerRRect);
    return Path.combine(PathOperation.difference, outerRectPath, innerRectPath);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
```

```dart
class GradientBorderContainer extends StatelessWidget {
  GradientBorderContainer({
    @required gradient,
    @required this.child,
    @required this.onPressed,
    this.strokeWidth = 4,
    this.borderRadius = 64
  	}) : this.painter = GradientPainter(
      gradient: gradient, strokeWidth: strokeWidth, borderRadius: borderRadius
  );

  final GradientPainter painter;
  final Widget child;
  final VoidCallback onPressed;
  final double strokeWidth;
  final double borderRadius;
  final double padding;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
        painter: painter,
        child: child
    );
  }
}
```

It is quite simple: we create `RRect` (rounded rectangles) based on the given Rects we used above and a radius we let the caller define in the constructor of our `GradientBorderContainer` widget.

![Round gradient border around text](https://dev-to-uploads.s3.amazonaws.com/i/5sxaew6pyymia9epgbtd.png)

## Give it a padding

Looks better but there is still room for improvement. The text looks like it is too near to the border, it actually touches the border. So let's give it a padding.

```dart
class GradientBorderContainer extends StatelessWidget {
  GradientBorderContainer({
    @required gradient,
    @required this.child,
    @required this.onPressed,
    this.strokeWidth = 4,
    this.borderRadius = 64,
    this.padding = 16
  	}) : this.painter = GradientPainter(
      gradient: gradient, strokeWidth: strokeWidth, borderRadius: borderRadius
  );

  final GradientPainter painter;
  final Widget child;
  final VoidCallback onPressed;
  final double strokeWidth;
  final double borderRadius;
  final double padding;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
        painter: painter,
        child: Container(
          padding: EdgeInsets.all(padding),
          child: child
        )
    );
  }
}
```

Once again we touch the constructor of our `GradientBorderContainer` widget. We extend it by one parameter called padding which defaults to 16. We then use this padding to wrap a Container around the child widget with the respective padding.

![Round gradient border around text with padding](https://dev-to-uploads.s3.amazonaws.com/i/h4sm91kgdu52hpgzi0lu.png)

## Ripple effect

Looks great, doesn't it? Now we can focus our improvent on the actual interaction. Since it looks like a button, we want to give it a feedback once the user touches it. We go for the classic Material ripple effect.

```dart
class GradientBorderContainer extends StatelessWidget {
  GradientBorderContainer({
    @required gradient,
    @required this.child,
    @required this.onPressed,
    this.strokeWidth = 4,
    this.borderRadius = 64,
    this.padding = 16,
    splashColor
  }) :
  this.painter = GradientPainter(
    gradient: gradient, strokeWidth: strokeWidth, borderRadius: borderRadius
  ),
  this.splashColor = splashColor ?? gradient.colors.first;

  final GradientPainter painter;
  final Widget child;
  final VoidCallback onPressed;
  final double strokeWidth;
  final double borderRadius;
  final double padding;
  final Color splashColor;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: painter,
        child: InkWell(
          highlightColor: Colors.transparent,
          splashColor: splashColor,
          borderRadius: BorderRadius.circular(borderRadius),
          onTap: onPressed,
          child: Container(
            padding: EdgeInsets.all(padding + strokeWidth),
              child: child
          ),
        ),
    );
  }
```

The ripple effect can be achieved by using an `InkWell`. The splashColor determines the color of the circle that grows as long as you tap down. We set it to the first color of the gradient unless something else is provided. This way, the effect still looks cool when no extra color is given. The highlightColor is set to `Colors.transparent` because otherwise it defaults to a grey that makes it look worse in my opinion. The InkWell needs the borderRadius as well. If we omitted it the splash would exceed the borders of the child.

![Gradient border around text with ripple effect in Flutter](https://dev-to-uploads.s3.amazonaws.com/i/hijvs819vo994ddavpjo.gif)

## Yo dawg, I heard you like borders

There's one last thing I would like us to improve. I noticed, when I used gradients with repeated patterns that contain a white color or generally contain the same color as background, it becomes harder to see the border.

```dart
GradientBorderContainer(
  strokeWidth: 16,
  borderRadius: 16,
  gradient: LinearGradient(
    begin: Alignment.topLeft,
    end: Alignment(-0.2, -0.4),
    stops: [0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1],
    colors: [
      Colors.pinkAccent,
      Colors.pinkAccent,
      Colors.white,
      Colors.white,
      Colors.pinkAccent,
      Colors.pinkAccent,
      Colors.white,
      Colors.white,
    ],
    tileMode: TileMode.repeated,
  ),
  child: Text('NEED OUTER BORDER!',
      style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
  onPressed: () {},
);
```
![Gradient border around text with white color](https://dev-to-uploads.s3.amazonaws.com/i/ywq1imyw2u3qd0hv9p0v.png)


```dart
GradientBorderContainer(
  strokeWidth: 16,
  borderRadius: 16,
  gradient: LinearGradient(
    begin: Alignment.topLeft,
    end: Alignment(-0.2, -0.4),
    stops: [0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1],
    colors: [
      Colors.pinkAccent,
      Colors.pinkAccent,
      Colors.white,
      Colors.white,
      Colors.pinkAccent,
      Colors.pinkAccent,
      Colors.white,
      Colors.white,
    ],
    tileMode: TileMode.repeated,
  ),
  child: Text('NEED OUTER BORDER!',
      style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
  onPressed: () {},
);
```

That's why we need a border around the border. Let's call it `outline` to make a better distinction.

```dart
class GradientBorderContainer extends StatelessWidget {
  GradientBorderContainer({
    ...
    this.outlineWidth = 1
  }) :
  this.painter = GradientPainter(
    outlineWidth: outlineWidth
  ),
  this.splashColor = splashColor ?? gradient.colors.first;
  final double outlineWidth;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: painter,
        child: InkWell(
          highlightColor: Colors.transparent,
          splashColor: splashColor,
          borderRadius: BorderRadius.circular(borderRadius),
          onTap: onPressed,
          child: Container(
            padding: EdgeInsets.all(padding + strokeWidth + outlineWidth),
              child: child
          ),
        ),
    );
  }
  ...
}

class GradientPainter extends CustomPainter {
  GradientPainter({this.gradient, this.strokeWidth, this.borderRadius, this.outlineWidth});

  final Gradient gradient;
  final double strokeWidth;
  final double borderRadius;
  final double outlineWidth;
  final Paint paintObject = Paint();

  @override
  void paint(Canvas canvas, Size size) {
    if (outlineWidth > 0) {
      _paintOutline(outlineWidth, size, canvas);
    }

    Rect innerRect = Rect.fromLTRB(
        strokeWidth, strokeWidth, size.width - strokeWidth, size.height - strokeWidth
    );
    RRect innerRoundedRect = RRect.fromRectAndRadius(innerRect, Radius.circular(borderRadius));

    Rect outerRect = Offset.zero & size;
    RRect outerRoundedRect = RRect.fromRectAndRadius(outerRect, Radius.circular(borderRadius));

    paintObject.shader = gradient.createShader(outerRect);
    Path borderPath = _calculateBorderPath(outerRoundedRect, innerRoundedRect);
    canvas.drawPath(borderPath, paintObject);
  }

  void _paintOutline(double outlineWidth, Size size, Canvas canvas) {
    Paint paint = Paint();
    Rect innerRectB = Rect.fromLTRB(
        strokeWidth + outlineWidth,
        strokeWidth + outlineWidth,
        size.width - strokeWidth - outlineWidth,
        size.height - strokeWidth - outlineWidth
    );
    RRect innerRRectB = RRect.fromRectAndRadius(innerRectB, Radius.circular(borderRadius - outlineWidth));

    Rect outerRectB = Rect.fromLTRB(-outlineWidth, -outlineWidth, size.width + outlineWidth, size.height + outlineWidth);
    RRect outerRRectB = RRect.fromRectAndRadius(outerRectB, Radius.circular(borderRadius + outlineWidth));

    Path borderBorderPath = _calculateBorderPath(outerRRectB, innerRRectB);
    paint.color = Colors.black;
    canvas.drawPath(borderBorderPath, paint);
  }

  Path _calculateBorderPath(RRect outerRRect, RRect innerRRect) {
    Path outerRectPath = Path()..addRRect(outerRRect);
    Path innerRectPath = Path()..addRRect(innerRRect);
    return Path.combine(PathOperation.difference, outerRectPath, innerRectPath);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
```

Another argument is added to the constructor: `outlineWidth`. It determines whether an outline should be visible and what the width of that outline should be.

![Gradient border around text with white color and outline](https://dev-to-uploads.s3.amazonaws.com/i/2rr4x631ygvneglahcag.png)

# Partial border

So far, we have drawn a border that is drawn continuously. Let's look at something new: trying to draw only the top left corner and the bottom right corner.

```dart
class PartialPainter extends CustomPainter {
  PartialPainter({this.radius, this.strokeWidth, this.gradient});

  final Paint paintObject = Paint();
  final double radius;
  final double strokeWidth;
  final Gradient gradient;

  @override
  void paint(Canvas canvas, Size size) {
    Rect topLeftTop = Rect.fromLTRB(0, 0, size.height / 4, strokeWidth);
    Rect topLeftLeft = Rect.fromLTRB(0, 0, strokeWidth, size.height / 4);

    Rect bottomRightBottom = Rect.fromLTRB(size.width - size.height / 4, size.height - strokeWidth, size.width, size.height);
    Rect bottomRightRight = Rect.fromLTRB(size.width - strokeWidth, size.height * 3 / 4, size.width, size.height);

    paintObject.shader = gradient.createShader(Offset.zero & size);

    Path topLeftPath = Path()
      ..addRect(topLeftTop)
      ..addRect(topLeftLeft);

    Path bottomRightPath = Path()
      ..addRect(bottomRightBottom)
      ..addRect(bottomRightRight);

    Path finalPath = Path.combine(PathOperation.union, topLeftPath, bottomRightPath);

    canvas.drawPath(finalPath, paintObject);
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
```

f you have seen and understood the previous examples, then this is probably not too hart to understand. We draw four rectangles, each representing a side of the indicated border. The length of the respective rectangles is dependent on the height of the child widget (a quarter of that). We then use `PathOperation.union` to combine the paths.

![Partial border with gaps in Flutter](https://dev-to-uploads.s3.amazonaws.com/i/h4rztlakx95qu06bqabw.png)

# Final thoughts

Using a `CustomPainter`, it's possible to achieve a lot more flexibility when it comes to drawing a border around a widget. In this tutorial, aside from the basics, we have learned how to draw a configurable gradient border around a widget. We are able to set a gradient, padding, a stroke width and the width of the outline around the border. On top of that, to indicate user interaction we have a ripple effect provided by an InkWell widget. Additionally, we have seen how it's possible to have only parts of the border drawn (by leaving gaps).

[GET THE FULL CODE](https://gist.github.com/flutter-clutter/66f5978d5056daa8265bc903db8c6c30)

[VIEW THE ORIGINAL TUTORIAL](https://www.flutterclutter.dev/flutter/tutorials/how-to-add-a-border-to-a-widget/2020/587/)