Search code examples
htmlcsspseudo-class

CSS combination of :active and :not psuedo classes


I have a code where the "card" class takes the user to an article when clicked. And its child, "category", takes the user to another website when clicked.

<div class="card">
    <div class="img"></div>
    <div class="category"></div>
    <div class="title"></div>
    <div class="description"></div>
</div>

enter image description here

I'm trying to write CSS animations with div:active for these two divs. So when I have the following, the whole card animates:

.card:active {
    transform: translateX(50px);
    transition: all 0.5s ease;
}

But I don't want the card to animate when the user clicks on the category div. So, I tried something like the following, and others, which didn't work.

:not(.category).card:active{
    transform: translateX(50px);
    transition: all 0.5s ease;
}

Is there a combination of :not and :active pseudo classes that I could use to make the card animate when clicked, but not animate when the category is being clicked?


Solution

  • solution 1, CSS only

    Generally one could use...

    /* CSS */
    .card                 { pointer-events: none }
    .card>:not(.category) { pointer-events: auto } /* all kids except .category */
    .card:active          { transform: translateX(50px) }
    
    /* HTML */
    <div class="card">
        <div class="img">image</div>
        <div class="category">CATEGORY</div>
        <div class="title">no-card</div>
        <div class="description">description</div>
    </div>
    

    ...and clicking any child of .card, except class .category, will trigger the card :active event as well as :hover. However, any card space not occupied by child elements wil not trigger any event (i.e. .card:padding and .category will not trigger :active or :hover).

    Another drawback is that .category will listen to no events at all and therefore cannot be an input element that needs to handle those events (like a <button>, as shown in the demo).

    If this is acceptable, this solution is the easiest to code and maintain.

    solution 2, CSS plus JS

    This solution uses only simple CSS...

    .effect:active { transform: translateX(50px) } /* NOT .card:active */
    

    ...and some Vanilla Javascript (pseudo code) that simply removes/adds the .effect class from .card when appropriate.

    forEach cardList.item do
       card.onmouseover = enableEffect();
    
       card.category.onmouseenter = disableEffect();
       card.category.onmouseout   = enableEffect();
    
    disableEffect = remove class 'effect' from .card
    enableEffect  = add class 'effect' to .card
    

    The below snippet includes both solutions, is heavily commented and includes a few responsiveness extras (like CSS columns, main font and page spacing. Math used MathIsFun: Linear Equation).

    Just copy the code and have fun with it!

    SNIPPET

    'use-strict';
    
    // Traverse an array and execute the passed callback function for each array element found
    var forEachEntryIn = function (array, callback, scope) {
            for (var i = 0; i < array.length; i++) { callback.call(scope, i, array[i]); } };
    
    // Get the list of cards
    var cards = document.getElementsByClassName('card');
    
    // Make this a function and you can toggle it with a <button>
    var DEBUG = false; // set to 'true' for debug view and some console output
    (DEBUG) ? document.body.setAttribute('outlines','1') : document.body.setAttribute('outlines','0');
    
    // Traverse the list of cards
    forEachEntryIn( cards,
        function (idx,card,scope) {
    
            // '.effect' is needed by default,
            // so why add it in HTML class="" property when we can do it here...
            card.classList.add('effect'); // remove if you want to assign in HMTL anyway
    
            /*
                MOUSEOVER events are bubbled to child elements
                MOUSEENTER does not bubble, needed on '.category'
    
                target: the element that triggered the event ('.card' OR any of its child elements)
                currentTarget: the element that the event listener is attached to: '.card'
            */
            card.onmouseover = function(e) { // Attach 'MOUSEOVER' listener to '.card'
                // Parent check: event may be bubbled (from any '.card' children)
                // So, is the parent a '.card' or maybe its parent?
                if (e.target.parentElement == e.currentTarget) {
                    enableEffect(e.target.parentElement); // Activate '.card' animation
                }; 
                // NOTE: Disable the check, click a card and see what happens....funny!
            };
    
            var category = card.querySelector('.category');
    
            if (card.contains(category)) {
                category.onmouseenter = function(e) { disableEffect(e.currentTarget.parentElement); };
                category.onmouseout   = function(e) { enableEffect (e.currentTarget.parentElement); };
            };
    
        } // end function (idx,el,scope)
    ); // end forEachEntryIn
    
    // Helper functions to keep main loop readable
    function enableEffect(parent) {
        if (!parent.classList.contains('effect')) { // if parent has no '.effect'
            parent.classList.add('effect'); // then add it
        };
        if (DEBUG) logInfo(parent);
    };
    
    function disableEffect(parent) {
        if (parent.classList.contains('effect')) { // parent if has '.effect'
            parent.classList.remove('effect'); // then remove it
        };
        if (DEBUG) logInfo(parent);
    };
    
    // For debugging
    function logInfo(p) {
        console.log( // Show some info in browser console
            ((p.className) ? '<' + p.tagName +' class="' + p.className + '">': '<' + p.tagName +'>' ),
            p.classList.contains('effect')
        );
    };
    /********************************/
    /* demo for CSS only solution 1 */
    /********************************/
    .no-card {
        pointer-events: none;
    } 
    .no-card>:not(.category) {
        pointer-events: auto;
    }
    .no-card:active {
        transform: translateX(50px);
        transition: all 0.5s ease;
    }
    /***********************************/
    /* demo for CSS plus JS solution 2 */
    /***********************************/
    /* class will be assigned with JS */
    .effect:active {
        transform: translateX(50px);
        transition: all 0.5s ease;
    }
    
    
    /*****************************************************/
    /* below just demo, everything can be safely removed */
    /*****************************************************/
    
    
    /**************************/
    /* preferred global rules */
    /**************************/
    html,body               { box-sizing: border-box; width: 100%; max-width: 100% }
    *::before,*::after, *   { box-sizing: inherit }
    body                    { margin: 0 }
    
    /*  ALL math reference: https://www.mathsisfun.com/equation_of_line.html */
    
    /* responsive base font size using y = mx + b */
    html   { font-size: calc(0.625vmin + 0.75rem) } /* (320,14)(1280,20) */
    
    /* prohibit user from selecting text (put in <body>) */
    [no-select] { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none }
    [do-select] { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; cursor: auto }
    /* enable user to select text (put in specific elements) */
    
    /* to show all elements with outlines (assigned to <body> with JS) */
    [outlines="1"] * { outline: 1px dashed }
    
    /***********************************/
    /* Extra: plain responsive columns */
    /***********************************/
    body {
        /*
            responsive page padding using y = mx + b
            p1(320,32) p2(1920, 72) => y = 0.025x + 24
            p3(320, 8) p4(1920,320) => y = 0.195x - 54.4 
        */
        padding: calc(2.5vh + 24px) calc(19.5vw - 54.4px);
    
    }
    .cardList {
        column-count: 3;    /* preferred number of columns given column-width */
        column-gap: 0;      /* handled with card margins */
    
        /*
            column width using y = mx + b
    
            mobile/tablet, 1 column : 320 - 60 = 260px
                  desktop, 3 columns: (1920 - 640) / 3 = 426 minus animation gap = 376px
    
            p1(320,260) p2(1920,376)
            => y = 7.25x + 236.8
        */
        column-width: calc(7.25vw + 230.8px); /* (320,260)(1920,376) */
        /* (320,260)(1920,376) for scrollbar => 236.8 - (18/3) = 230.8px */
    }
    .card {
        break-inside: avoid; /* don't split card over columns */
    }
    
    /******************/
    /* card eye-candy */
    /******************/
    .wrapper,
    .cardList {
        background-color: rgba(0,0,0,.1); /* just to review body padding */
        padding: 2rem 0;
    }
    
    .no-card, .card {
        background-color: CornSilk;
    
        padding: 1rem;
        margin : 1rem;
        margin-right: 60px;  /* animation width plus 10px space */
    
        /* GMC elevation 1dp */
        box-shadow: 0px 2px 1px -1px rgba(0,0,0,.20),
                    0px 1px 1px  0px rgba(0,0,0,.14),
                    0px 1px 3px  0px rgba(0,0,0,.12);
    }
    .card:first-child { margin-top: 0 } /* otherwise jagged column tops */
    
    /* Some :hover animation */
    .no-card:hover, .card:hover {
        /* GMC elevation 3dp */
        box-shadow: 0px 3px 3px -2px rgba(0,0,0,.20),
                    0px 3px 4px  0px rgba(0,0,0,.14),
                    0px 1px 8px  0px rgba(0,0,0,.12);
    }
    <body no-select>
    
    <h2>solution 1, CSS only</h2>
    <div class="wrapper">
        <div class="no-card">
            <div class="img">image</div>
            <button class="category">CATEGORY</button>
            <div class="title">no-card</div>
            <div class="description">description</div>
        </div>
    </div>
    
    <h2>solution 2, CSS plus JS</h2>
    <div class="cardList">
        <div class="card">
            <div class="img">image</div>
            <button class="category">CATEGORY</button>
            <div class="title">card 1</div>
            <div class="description">description</div>
        </div>
    
        <div class="card">
            <div class="img">image</div>
            <button class="category">CATEGORY</button>
            <div class="title">card 2</div>
            <div class="description">description</div>
        </div>
    
        <div class="card">
            <div class="img">image</div>
            <button class="category">CATEGORY</button>
            <div class="title">card 3</div>
            <div class="description">description</div>
        </div>
    
        <div class="card">
            <div class="img">image</div>
            <button class="category">CATEGORY</button>
            <div class="title">card 4</div>
            <div class="description">description</div>
        </div>
    
        <div class="card">
            <div class="img">image</div>
            <div>some other element</div>
            <div class="title">card 5</div>
            <div class="description">description</div>
        </div>
    
        <div class="card">
            <div class="img">image</div>
            <button class="category">CATEGORY</button>
            <div class="title">card 6</div>
            <div class="description">description</div>
        </div>
    </div>
    </body>