Always Twisted

CSS Logical Properties, easier with Sass

On This Page:

Using logical properties when writing CSS allows your code to adapt more naturally to various text directions and writing modes, such as left-to-right (LTR), right-to-left (RTL), or even vertical text.

Unlike traditional properties like margin-left or padding-top, logical properties help future-proof your designs by removing assumptions about content flow and the needs of your site visitors.

Rather than thinking in fixed directions, logical properties enable your designs to adapt automatically. This improves both consistency and flexibility, making your CSS more scalable and versatile for the different contexts it may encounter.

The problem with logical properties

They can be long, really long.

Whilst logical properties are a fantastic way forward offering flexibility and adaptability, they can introduce a level of verbosity that can make your code appear more cumbersome. Instead of using margin-left, you would need to write margin-inline-start, which can feel longer and less intuitive.

Going all in on using logical properties and as more are used, such as border-block-end-width, border-inline-start-color, the increased verbosity can make your stylesheets harder to read and potentially harder to maintain.

Let’s make things easier with Sass

One approach to handle the potential verbosity and cognitive load of using logical properties is to make use of the (still) widely used preprocessor, Sass. Using logical properties can feel to repetitive and could require extra thought as you translate the more traditional properties into their newer logical counterparts.

By using Sass with its functions, maps, variables, and mixins, you can simplify the syntax, reduce any possible mental overhead, and allow for more readable, reusable, and intuitive code that will automatically handle the complexity of logical property mapping.

The Sass we need

When thinking about how I could use the features of Sass to make it easier to write CSS logical properties I thought about what properties to work with and what CSS values to be able to apply.

Although CSS logical properties go way beyond these examples, I thought It would be good to limit the amount of options to the CSS I write day-to-day that could benefit from these mixins. This boiled down to margin, padding, border and it’s derivatives including border-radius as it’s slightly different as it’s more ‘corners’ thant ‘sides.

I also thought that we should be able to allow the author to write in several ways which are: a raw CSS value, a Sass $variable (we’re using Sass after all) or a CSS custom property. Deciding on this as ‘the brief’ I started to work on writing the code.

A function for values

In a Sass mixin, passing through a raw CSS value or Sass $variable is relatively straightforward but when it comes to using CSS custom properties in Sass you will eventually have to deal with interpolation. I wanted the value an author prescribes to be as less taxing as possible. I wanted an author ot be able to write

To avoid having to write the interpolation and for the author not to have to write var() each time I created a small little Sass function that checks the start of the $value, if it contains -- then it will return the $value as a CSS custom property wrapped in var(). If the value doesn’t contain -- at the start it simply returns the value.

Code languagescss
@function slp-process-value($value) {
@if type-of($value)=='string' and str-index($value, '--')==1 {
@return var(#{$value});
}
@else {
@return $value;
}
}

Creating maps for cleaner mixins

My first efforts in creating a Sass mixin had me using the @if and @else Sass control directives.

Code languagescss
@mixin logical-margin($side, $value) {
@if $side == top {
margin-block-start: $value;
} @else if $side == bottom {
margin-block-end: $value;
} @else if $side == left {
margin-inline-start: $value;
} @else if $side == right {
margin-inline-end: $value;
}
}

An author could then write:

Code languagescss
@include logical-margin(top, 30px)

which would output in a stylesheet as:

Code languagecss
margin-block-start: 30px

Although the person authoring may never need to touch or see these mixins I think it would be cleaner, and less repetitive when writing more mixins to make use of Sass maps. We want to be able to not just port the complexity of the @if / @else statement to the Sass map but to simplify the possibilities so we can reuse the map as much as possible.

As we will be wanting to use these logical property Sass mixins for padding, margin, and border we can create a Sass map and mixins to not need those words. Instead tying the Sass map to margin, for example:

Code languagescss
$logical-margin-map: (
top: margin-block-start,
bottom: margin-block-end,
left: margin-inline-start,
right: margin-inline-end
);

We can simplify it and let the Sass mixin do some work:

Code languagescss
$logical-map: (
top: block-start,
bottom: block-end,
left: inline-start,
right: inline-end
);

With the above Sass map we can reuse it in our mixins for border, margin, and padding.

Making the mixin

So, we have our Sass function that works out if the $value passed is a CSS custom property or not and we have a Sass map that helps the simplifying and tying together of traditional CSS properties and the newer logical properties. We need a mixin to put it all together.

So for the following examples I will concentrate on just on of the possible mixins. A Sass mixin for easier logical properties for with margin.

We want our Sass mixin to take two variables: the $side that we want the rule to change and the $value of how much to apply. To determine if the Sass mixin is dealing with a raw CSS value, a Sass $variable or a CSS custom property we want the $value to go through our function we have created. To determine which $side we want to effect and have the correct logical property printed out in CSS we need fetch that correct value from the map. Then, if the $side value is correct (top,bottom,left,right) we want to generate the CSS.

Code languagescss
@mixin logical-margin($side, $value) {
$processed-value: slp-process-value($value);
$logical-side: map-get($logical-map, $side);
@if $logical-side {
#{'margin-' + $logical-side}: #{$processed-value};
}
@else {
@warn "Invalid side `#{$side}` specified for logical-margin mixin.";
}
}

We would then write out similar mixins for the padding and border rules.

Using the mixin

Now we have the Sass function, map, and mixin we can then use it in our codebase:

:root {
--padding-l: 2rem;
}
$margin: 4rem;
body {
@include logical-border-width(top, 1rem);
@include logical-border-color(top, #BADA55);
@include logical-border-style(top, solid);
@include logical-margin(top, $margin);
@include logical-padding(left, --padding);
@include logical-padding(right, --padding);
}

Which would generate this CSS:

Code languagecss
:root {
--padding-l: 2rem;
}
body {
border-block-start-width: 1rem;
border-block-start-color: #BADA55;
border-block-start-style: solid;
margin-block-start: 4rem;
padding-inline-start: var(--padding);
padding-inline-end: var(--padding);
}

border-radius hits different

As I previously mentioned unlike CSS properties for rules like margin and padding that affect the sides of an element on the page border-radius logical properties effect the corners and because of this the syntax may seem a little odd. Rather than border-top-left-radius with logical properties you can write border-start-start-radius.

To make use of Sass to simplify writing CSS for border-radius using logical properties we can still use the Sass function above, but we will need a new Sass map to accommodate the different syntax and another mixin to generate the final code.

For the Sass map we can write something similar to the $logical-map map we created earlier.

Code languagescss
$logical-border-radius-map: (
top-left: start-start,
top-right: start-end,
bottom-left: end-start,
bottom-right: end-end
);

As the CSS for the border-radius is written differently to something like margin or padding we would need a new Sass mixin that accommodates this, putting the logical property between border- and radius. This is a relatively simple re-jig of the Sass mixin we have already created.

Code languagescss
@mixin logical-border-radius($corner, $value) {
$processed-value: slp-process-value($value);
$logical-corner: map-get($logical-border-radius-map, $corner);
@if $logical-corner {
#{'border-' + $logical-corner + '-radius'}: #{$processed-value};
}
@else {
@warn "Invalid corner `#{$corner}` specified for logical-border-radius mixin.";
}
}

Wrapping up, I hope this post highlights how Sass can be a powerful tool for managing CSS logical properties in your everyday workflow. By leveraging Sass to handle the potential verbosity and complexity of logical properties, you can keep your codebase clean and easier to maintain, even as your designs scale and evolve. This approach simplifies the process, ensuring your CSS remains adaptable and efficient, without getting bogged down by repetitive or lengthy syntax.

You can start using the Sass Logical Properties package right away, or if you’re curious about how it works, head over to the GitHub repo to view the source code. With these tools, you’ll find working with logical properties far more manageable in your next project.

I realise that while Sass simplifies logical properties, adding a build step isn’t always the easiest solution for everyone. However, for those already using Sass, it can help reduce complexity, improve maintainability, and enhance readability.

Are your stakeholders unclear about the value your Design System brings to the team?

I’ll help you demonstrate the ROI and benefits of your Design System.

get in touch!