Proper Dotted Borders
6 minute readUse SASS to make dotted borders without weird corners.
CSS has dotted and dashed borders. They’re fine but don’t always look the way you might expect or prefer.
border-style: dotted
dots are round (on my site I wanted square)border-style: dashed
dashes are nice and sharp but on corners the dashes become L shapes- Both dots and dashes are drawn inconsistently depending on how thick they are.
9-Slice Scaling
9-slice scaling is a technique for defining how images scale that’s been around since at least the days of Macromedia Flash. It’s typically used when we want to scale a border and avoid the ugly distortion we’d encounter if we were to just naively stretch an image.
An image is sliced up into 9 pieces, each piece being used to draw a specific edge or corner of a border. The top, bottom, left and right edges stretch (or repeat) to fit the content, but the corners are not scaled so they don’t distort. When it’s all put together we get a nice clean border.
9-slicing was a common way of achieving rounded corners on the web before we had border-radius
. In those days it required assembling all of the image slices into a <table>
or similar, then using that element as a background for the thing we wanted bordered. Hacky, but it worked and if you wanted rounded corners that didn’t look like hockey sticks before 2015, it’s what you did.
CSS border-image
CSS now has border-image which lets us use the 9-slice method properly. It depends on a specific, static image though, meaning if we want borders of different colours we have to create a separate image for each color. Particularly annoying while we’re experimenting with different colours.
SASS can Change the Fill Colour of an SVG
We can get around this by using an SVG image and encoding it as a Data URI (ie url("data:image/svg...")
), only instead of the usual fill colour inside the SVG, we use placeholder text eg FILLCOLOR
. We can then use SASS to find/replace that string with whichever colour we want.
String replacement
SASS doesn’t directly have a string replacement function but it’s simple to add one. I found one from CSS Tricks (I had to update it a little because SASS string function names have changed since it was written).
As you can see SASS has string.index()
, which is just like String.prototype.lastIndexOf()
in JavaScript (it gives us the character position/index of the matching string). SASS also has string.length()
, so getting from those two functions to a str-replace
is straightforward.
@use "sass:string";
/// Replace `$search` with `$replace` in `$string`
/// @author Kitty Giraudel
@function str-replace($string, $search, $replace: "") {
$index: string.index($string, $search);
@if $index {
$string: string.slice($string, 1, $index - 1) +
$replace +
str-replace(
string.slice($string, $index + string.length($search)),
$search, $replace
);
}
@return $string;
}
Getting the coloured image
We’ll stick to hex colours, because the URI encoding for those is really simple: the #
just has to be converted to %23
, so eg #ed397d
becomes %23ed397d
.
The SVG data is stored in a SASS variable $dotted-border-image-enc
.
Using the str-replace
function from above we can create a border-image-colored
function which takes a hex colour, inserts it into $dotted-border-image-enc
, and returns a url("data:image/svg...")
which can be used as our border-image-source
in CSS.
// SVG string, note the `style='fill:FILLCOLOR;`
$dotted-border-image-enc: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cg style='fill:FILLCOLOR'%3E%3Cpath d='M10 10h10v10H10zM70 10h10v10H70zM40 40h10v10H40zM40 10h10v10H40zM10 70h10v10H10zM40 70h10v10H40zM70 70h10v10H70zM70 40h10v10H70zM10 40h10v10H10z'/%3E%3C/g%3E%3C/svg%3E";
@function border-image-colored($color_hex) {
// Replace hex colour with url-encoded colour
$enc-color: str-replace("" + $color_hex, "#", "%23");
$img-url: str-replace(
$dotted-border-image-enc,
"FILLCOLOR",
$enc-color
);
@return url($img-url);
}
Getting our sliced image border
Now we have a coloured image we can slice it up and apply it to borders. We’ll make a SASS @mixin
that we can re-use wherever we want a dotted border.
The CSS border-image-*
properties interact with each other in ways which can be confusing. I’ve used values here that worked with my image but check the MDN docs for border-image and play around.
A solid border is drawn for a split second while the border image loads. To prevent a flicker, we set border-color
to match the background.
@mixin dotted-border($color_hex) {
border-style: solid;
border-width: 30px;
border-image-width: 30px;
border-image-slice: 30;
border-image-repeat: round;
// Prevent flash of foreground colour by setting the initial colour to match the page background
border-color: white;
// Call the function to get the SVG Data URI
border-image-source: border-image-colored(#{$color_hex});
}
Allowing for optional border sides
Sometimes we might want to only show the border on specific sides of its element so we’ll give the mixin a $sides...
argument. This means “take all remaining arguments (after $color_hex
) and make them members of a list called $sides
”. If $sides
is not provided we’ll draw the full border.
It might look more complicated but the main difference is instead of setting eg border-style: solid
as a whole, we set border-right-style: solid
and so on, depending on which sides we want to draw.
@mixin dotted-border($color_hex, $sides...) {
// biw = border image width
$biw_top: "initial";
$biw_left: "initial";
$biw_right: "initial";
$biw_bottom: "initial";
$border_width: 30px;
$border_image_width: 30px;
// Ensure we're starting clean
border-style: none;
@if list.length($sides) > 0 {
// For each specified border, set its style and width ready to receive the image
@each $side in $sides {
@if $side == "left" {
border-left-style: solid;
border-left-width: #{$border_width};
$biw_left: #{$border_image_width};
} @else if $side == "right" {
border-right-style: solid;
border-right-width: #{$border_width};
$biw_right: #{$border_image_width};
} @else if $side == "top" {
border-top-style: solid;
border-top-width: #{$border_width};
$biw_top: #{$border_image_width};
} @else if $side == "bottom" {
border-bottom-style: solid;
border-bottom-width: #{$border_width};
$biw_bottom: #{$border_image_width};
}
}
border-image-width: #{$biw_top} #{$biw_right} #{$biw_bottom} #{$biw_left};
} @else {
// No `$sides` specified so default to drawing all sides
border-style: solid;
border-width: #{$border_width};
border-image-width: #{$border_image_width};
}
// Prevent flash of foreground colour by setting the initial colour to match the page background
// (pretending the page is white for this example)
border-color: white;
border-image-repeat: round;
border-image-source: border-image-colored(#{$color_hex});
// This should match the size of your corner slices but you can experiement with it
border-image-slice: 30;
}
Call it like this:
p {
@include dotted-border(#ed397d, top, bottom);
}
Which will convert into:
p {
border-style: none;
border-style: solid;
border-image-width: 30px;
border-color: white;
border-image-repeat: round;
border-image-source: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cg style='fill:%23ed397d'%3E%3Cpath d='M10 10h10v10H10zM70 10h10v10H70zM40 40h10v10H40zM40 10h10v10H40zM10 70h10v10H10zM40 70h10v10H40zM70 70h10v10H70zM70 40h10v10H70zM10 40h10v10H10z'/%3E%3C/g%3E%3C/svg%3E");
border-image-slice: 30;
}
and this is what our custom border-image
looks like
compared to CSS border-style: dashed
...
... or CSS border-style: dotted