HTML & CSS
Article

A Bulletproof Function to Validate Length Values in Sass

By Hugo Giraudel

Once you start getting comfortable with Sass, playing with some functions and writing your own mixins, soon enough you will want to validate developer input. This means adding some extra checks to your functions and mixins to make sure the given arguments are correct in terms of how they are going to be used within the function or mixin.

After having seen a dozen different functions supposedly aimed at validating a length, I thought I would give it a try and write something on it. So here we are.

Let’s first recall what a length is according to official CSS specifications:

Lengths refer to distance measurements […] A length is a dimension. However, for zero lengths the unit identifier is optional (i.e. can be syntactically represented as the ‘0’). A dimension is a number immediately followed by a unit identifier.

If we sum up what we just read, a length is a number followed by a unit, unless it’s zero. It’s a bit simplistic but we’ll start with that.

Setting Up the Function

If a function returns a Boolean, it is usually good practice to name the function with a leading is-. By the very nature of the question is [this] [that]?, we imply that the answer is either yes (true) or no (false).

In our case, we want to make sure the given value is a length, so I suggest is-length, in all its simplicity.

/**
 * Check whether `$value` is a valid length.
 * @param {*} $value - Value to validate.
 * @return {Bool}
 */
@function is-length($value) {
  // Do some magic and return a Boolean.
}

Returning a Valid Value

Thanks to our refresher we know that a length is a number with a unit. Let’s start with that.

@function is-length($value) {
  @return type-of($value) == "number" and not unitless($value);
}

Before going any further, let me remind you of a couple of things:

  • We return a Boolean, so there is no need to pollute the function’s core with unnecessary @if, @else if and @else directives.
  • We quote strings (using double quotes) because it’s more elegant and because an unquoted string is strictly equal to the same quoted string anyway.

Okay, that’s a good start. Unfortunately, most Sass frameworks that do unit validation will stop there. We cannot stop after such a good start! Also, we failed to implement correctly the CSS specification that if the value is zero, the unit is optional. Our function will return false if $value is 0 because it is unitless. Let’s fix it!

@function is-length($value) {
  @return $value == 0 or type-of($value) == "number" and not unitless($value);
}

The or (||) operator is implemented like this in most languages: Keep parsing the condition until a token evaluates to true. That means if $value is 0, the function stops parsing and directly returns true, otherwise it keeps going.

Note that we could have written our condition like this as well (where parentheses are included for readability):

@function is-length($value) {
  @return type-of($value) == "number" and (not unitless($value) or $value == 0);
}

Our first implementation reads as if $value is 0 or $value is a number with a unit. While this implementation reads as if $value is a number and either has a unit or is 0. You can pick the one you feel the most comfortable with.

Allowing Common Values

You are probably not without knowing that there are a couple of values that can apply to all CSS properties: initial and inherit. We can also add auto to the list because most length-based properties do accept auto as a valid value. So we have three values that we want to allow, four if we count 0 that we already added previously. Let’s start with the ugly way:

@function is-length($value) {
  @return $value == 0 
          or $value == "auto" 
          or $value == "initial" 
          or $value == "inherit" 
          or type-of($value) == "number" 
          and not unitless($value);
}

Wow… Not really elegant, is it? Thankfully, in a previous article here at SitePoint I showed you how we can optimise such a statement using the index function.

The index function returns the index of the second parameter (our value) in the first parameter (a list). If the value isn’t found in the list, it returns null in Sass 3.4 or false in Sass 3.3.

@function is-length($value) {
  @return index(0 "auto" "inherit" "initial", $value) 
          or type-of($value) == "number" 
          and not unitless($value);
}

The thing is, our is-length value can now return an integer. For instance, if $value is auto, then the function returns 2, because auto is the second item in our list of allowed values. While it shouldn’t be a big deal if you use @if is-length($value), there might be cases where you want to have a Boolean at all times.

In order to make sure the function returns a bool type, you can use the not-not trick (seen in the same article as above) to cast the value as a Boolean. While this is not really elegant, it works fine:

@function is-length($value) {
  @return not not index(0 "auto" "inherit" "initial", $value) 
          or type-of($value) == "number" 
          and not unitless($value);
}

If you want to make this more explicit, you can build a little contains function that hides this:

/**
 * Check whether `$list` contains `$value`.
 * @param  {List} $list  - List of values.
 * @param  {*}    $value - Value to check in the list.
 * @return {Bool}
 */
@function contains($list, $value) {
  @return not not index($list, $value);
}

…then:

@function is-length($value) {
  @return contains(0 "auto" "inherit" "initial", $value) 
          or type-of($value) == "number" 
          and not unitless($value);
}

Both clear and eloquent, but needs an extra function. I’ll let you decide which is best for your needs.

Note: remember that auto is an invalid value for padding. Quite an edge case, but you should keep that in mind.

Allowing calc()

Our function is starting to get somewhere, isn’t it? However, we still need to allow calc(), which is often forgotten or omitted from length checkers. The thing with checking for calc() is that it requires Sass 3.3, because we need the str-slice method.

The idea is quite simple. We slice the first four characters from the value. If the result is “calc”, then it (likely) means the value is a calc() function, which is valid:

@function is-length($value) {
  @return not not index(0 "auto" "initial" "inherit", $value) 
          or type-of($value) == "number" and not unitless($value) 
          or str-slice($value + "", 1, 4) == "calc";
}

Two things to note here:

  1. We use $value + "" to cast the value as a string in case it’s a number (e.g. 42px) to avoid an error with the str-slice function expecting a string.
  2. We put this check at the very end to perform it only when needed since it is likely to be both less frequent and more costly than the other two.

The function looks pretty good so far!

Extending to Sizing

At this point we have quite a robust function to validate lengths. We could use it in a sizing mixin like this:

/**
 * Set `width` and `height` in a single statement.
 * @param {Number} $width - Width.
 * @param {Number} $height ($width) - Height.
 * @output `width` and `height`.
 * @requires {function} is-length
 */
@mixin size($width, $height: $width) {
  @if is-length($width) {
    width: $width;
  }

  @else {
    @warn "Invalid length `#{$width}` for `$width` parameter in `size` mixin.";
  }

  @if is-length($height) {
    height: $height;
  }

  @else {
    @warn "Invalid length `#{$height}` for `$height` parameter in `size` mixin.";
  }
}

If you try it, you’ll see it works pretty well. I should note that we might have a problem if we try to use intrinsic sizing. In case you are not familiar with that concept, intrinsic sizing involves new valid values for both width and height properties in order to give more control to developers over the size of their elements.

The thing is, we cannot update our function to include intrinsic values since they are invalid for most properties using lengths (padding, margin, background-size, and so on). What we could do, however, is make another function using the one we already built. What about is-size?

/**
 * Check whether `$value` is a valid size.
 * @param {*} $value - Value to validate.
 * @return {Bool}
 * @requires {function} is-length
 */
@function is-size($value) {
  @return is-length($value)
          or not not index("fill" "fit-content" "min-content" "max-content", $value);
}

…or if you use the contains function as well:

@function is-size($value) {
  @return is-length($value)
          or contains("fill" "fit-content" "min-content" "max-content", $value);
}

Now we can safely use intrinsic sizing values with our brand new is-size function without fearing a falsy return! Pretty neat, right?

Final Thoughts

As I’ve shown in this article, to build a bulletproof function to validate lengths, you have to make sure you cover the most frequent edge cases (calc(), intrinsic sizing, etc).

We could still improve the function by filtering angles (deg, rad, grad, turn) and durations (s, ms) because they are currently considered as lengths by our function since they are basically a number with a unit. But since lengths, angles, and durations are used in very different contexts, there’s very little chance that the function will get passed an angle when it’s expecting a length.

Covering edge cases is nice, but covering everything including the most unlikely scenario is sometimes excessive.

You can find the full code to play with on this Sassmeister.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.