Group Variants

@sgroup/tailwind-plugins/group-variants

You should hopefully be familar with the concept of Tailwind variants. There are some special types of variants, which "group" functionality together. One of which is the group-hover variant.

Group-hover example

Here, you can see hovering over the outer element actually triggers the colour of both inner elements. This is through a combination of group and group-hover classes to define the parent and child elements relationship, respectively.

Through this plugin, we can create completely new variants for grouping, quickly and easily. In your tailwind.config.js file:

theme: {
    extend: {
        groupVariants: {
            'group-parent': {
                groupSelector: 'group-child',
            },
        },
    },
},

variants: {
    extend: {
        display: ['group-parent'],
    },
},

Would produce CSS like:

.group-parent .group-child:block {
    display: block
}

.group-parent .group-child:inline-block {
    display: inline-block
}

...etc

Let's cover the theme.groupVariants first.

You can add multiple objects defining the selectors for parent and child selectors. You can also supply a groupPrefix and groupSuffix to add more configuration to the parent selector.

groupVariants: {
    'group-parent': {
        groupPrefix: 'body',
        groupSelector: 'group-child',
        groupSuffix: ':before',
    },

    'another-parent': {
        groupPrefix: '.some-parent',
        groupSelector: 'another-child',
    },
},

Which would produce:

body.group-parent:before .group-child:block {
    display: block
}

.some-parent.another-parent .another-child:block {
    display: block
}
...etc

The second part is to add the group variant you've defined to your variants configuration in order for them to actually used.

variants: {
    extend: {
        display: ['group-parent'],
    },
},

Here, we're enabling all display classes to generate the group-parent group variant we've defined.

There's plenty of use-cases for this, but let's explore a few.

Mobile Menu

A common pattern for a mobile menu is to show a slide-out when clicking on an icon in the header. A great way to handle this is to toggle a class on the <html> element, and then using CSS selectors, change behaviour based on this classes existance.

In CSS this would be trivial using something like:

.menu-open {
    .menu-overlay {
        @apply opacity-100 visible;
    }

    .menu-pane {
        @apply translate-x-0;
    }
}

But this relies on CSS, which we'd rather not do if we can help it, instead relying on utility classes. Not to mention every project might implement a mobile-menu differently - we want a bit more flexibility here.

Let's define a custom group variant to handle this.

TIP

This is already part of our @sgroup/tailwind-base package.

theme: {
    extend: {
        groupVariants: {
            // Add a `mobile-nav-open` group variant - `html.mobile-nav-open .mobile-nav-open:*`
            'mobile-nav-open': {
                groupSelector: 'mobile-nav-open',
                groupPrefix: 'html',
            },
        },
    },
},

variants: {
    extend: {
        opacity: ['mobile-nav-open'],
        display: ['mobile-nav-open'],
        translate: ['mobile-nav-open'],
        visibility: ['mobile-nav-open'],
    },
},

Here, we're creating a mobile-nav-open group variant as both the parent and child selectors, just for convenience. Putting this into action and combining with our Mobile Nav component to toggle the class on the body element:

Mobile menu example

Ignoring the fact that we toggle a mobile-menu-open class on the <html> element with JS, we're using the following to trigger this functionality:

<html>
    <div class="mobile-nav-open:opacity-100 mobile-nav-open:visible ...">
        <div class="...">
            <div class="...">
                <div class="mobile-nav-open:translate-x-0 ...">

Because we have enabled opacity, visibility and translate variants to be able to use mobile-nav-open, we can use any of their utility classes as variants. Loading the page, this would not apply any mobile-nav-open:* classes until there's a html.mobile-nav-open parent class selector.

 





<html class="mobile-nav-open">
    <div class="mobile-nav-open:opacity-100 mobile-nav-open:visible ...">
        <div class="...">
            <div class="...">
                <div class="mobile-nav-open:translate-x-0 ...">

Accordion

Similar to the above, you might want to trigger opening of an accordion, when a parent selector (the overall accordion) has a class set.

theme: {
    extend: {
        groupVariants: {
            // Add a `accordion-open` group variant - `.accordion-open .open:*`
            'accordion-open': {
                groupSelector: 'open',
            },
        },
    },
},

// Setup our `mobile-nav-open` group variant to be used on a few variants, by default
variants: {
    extend: {
        display: ['accordion-open'],
    },
},

Instead of binding to the <html> element but we're applying to the .accordion element.

Accordion example

Ignoring the fact that we toggle a open class on the <div> element with JS, we're using the following to trigger this functionality:

<div>
    <button onclick="parentElement.classList.toggle('open')">
        <div class="hidden accordion-open:block ...">

Because we have enabled opacity, visibility and translate variants to be able to use mobile-nav-open, we can use any of their utility classes as variants. Loading the page, this would not apply any mobile-nav-open:* classes until there's a html.mobile-nav-open parent class selector.

 



<div class="open">
    <button onclick="parentElement.classList.toggle('open')">
        <div class="hidden accordion-open:block ...">

Because we have enabled display variants to be able to use accordion-open, we can use any of their utility classes as variants. Loading the page, this would not apply any accordion-open:* classes until there's a .open parent class selector.