Common State Styling within Interactions

Within modern site development, it’s expected to style interactive elements which change when hovered, toggled, focussed or hit.

To keep things simple, say we’re required to style an element with children who have changing properties on :hover. We could write something like this:

// SCSS
.button {
background: white;
  &:hover {
background-color: black;
}
  &__child {
color: black;
    .button:hover & {
color: white;
}
    &:after {
content: '';
      .button:hover & {
color: white;
}
    }
}
}
/* Outputs to */
.button {
background: white;
}
.button:hover {
background-color: black;
}
.button .button__child {
color: black;
}
.button:hover .button__child {
color: white;
}
.button:hover .button__child:after {
content: '';
color: white;
}

The selector chain is required because we first have to check the parents state to style the children.

I find this can make styling these state-based interactions feel a little backward. Within the child, we reference the parent then back to the child again with the ampersand (.button:hover &).

Some other annoyances are:

  • The hover state selector on the “.button” element having to be different to the syntax used on the child elements (“&:hover” vs “.button:hover &”)
  • And having to reference the parent “.button” name with each hover surely isn’t a dry way of styling.
I started thinking how I could use a single, common way to style these children when the parent state changes.

Tossing Away Parent References

After working through a few options, I’ve arrived on a solution that’s added just the right amount of simplification to state-based styles.

I can style any of the children in a consistent and dry way, without having to reference the parent “.button” selector.

Here’s how we can now write our state-based styles:

// SCSS
.button {
@include base;
  background: white;
  @include baseHover {
background-color: black;
}
  &__child {
    @include baseHover {
color: white;
}
    &:after {
      @include baseHover {
color: white;
}
    }
}
}

Or if you prefer to keep your children selectors un-nested:

// SCSS
.button {
@include base;
  background: white;
  @include baseHover {
background-color: black;
}
}
.button__child {
  @include baseHover {
color: white;
}
}
.button__child:after {
  @include baseHover {
color: white;
}
}
A shining feature is the ability to use it directly on the “.button” element or within the “.button” children.

And here’s a glimpse at the code within the two mixins running the show:

@mixin base {
$this: nth(&,1) !global;
$hover: '#{$this}:hover &' !global;
}
@mixin baseHover {
@if (#{nth(&,1)}) == (#{$this}) {
&:hover {
@content;
}
} @else {
#{$hover} {
@content;
}
}
}

Let’s break it down

In the mixin ‘base’, the variable ‘$this’ is given the value of the direct parent it’s placed within by using ‘nth(&,1)’.

I use it to set the ‘$hover’ variable and add the ampersand at the end of the selector. This is the way to bake the context of the current child onto the end of the selector.

The ‘!global’ directive gives the variable a global scope instead of the default and limited mixin scope.

The ‘baseHover’ mixin simply checks if the current parent is the same as the base parent and then returns the right selector for the job.

Adding more selectors to the list

Since there’s more state selectors than just :hover, I’ve added the rest of them as mixins below. I was even able to combine :focus and :hover into a single mixin which simplifies things even more:

@mixin base {
$this: nth(&,1) !global;
$hover: '#{$this}:hover &' !global;
$focus: '#{$this}:focus &' !global;
$hocus: '#{$this}:focus &, #{$this}:hover &' !global;
$active: '#{$this}:active &' !global;
}
@mixin baseHover {
@if (#{nth(&,1)}) == (#{$this}) {
&:hover {
@content;
}
} @else {
#{$hover} {
@content;
}
}
}
@mixin baseFocus {
@if (#{nth(&,1)}) == (#{$this}) {
&:focus {
@content;
}
} @else {
#{$focus} {
@content;
}
}
}
@mixin baseActive {
@if (#{nth(&,1)}) == (#{$this}) {
&:active {
@content;
}
} @else {
#{$active} {
@content;
}
}
}
// And a mixin that includes :hover & :focus together
@mixin baseHocus {
@if (#{nth(&,1)}) == (#{$this}) {
&:hover, &:focus {
@content;
}
} @else {
#{$hocus} {
@content;
}
}
}

Take a look at these mixins in use within the button example below:

Last Words

I hope the mixins help to reduce the complexity of your state styles. Let me know how well they work for you and if you have any ideas to extend it further.

Happy styling (again) :)