Gallery 2020 - MK III

Note: This work has been a journey. I went from this model, to a model less reliant on magic numbers, to a change using srcset (this one!) then back to two images, but lazy loading for bandwidth preservation. Phew.

Received feedback on Version 2 from a Discord server I frequent:

    [04:16] fluffy: Ooh, neat
    [04:18] fluffy: you could also use img srcset to serve up a smaller rendition for the thumbnail
    [04:19] fluffy: also CSS transitions might make it a bit less jarring :slight_smile:

I thought that was a great solution to having the images load as full images, but hidden, before the user got a chance to decide whether or not to use them. What a bandwidth saver! I started playing with the code in my mind, enjoying the exercise and went to bed.

Woke up at 5:30am realising I'd missed a key point. I'm not using the Thumbnails as a navigation tool bar anymore, so only one version of the image is shown at any one time. That means I can use srcset to combine the Thumbnail and the Full version of the images into the one tag. THAT is a bandwidth saver. A small burst of excited coding later and I have this new version.

Gallery

How it works

Using cascading stylesheets you can walk down the tree (>) or look at siblings (+) but can't go backwards. How to reach back in the DOM tree to be able to grab the Previous image to turn it into a link (and snazzy thumbnail)? Without Javascript? Inputs!

With inputs you have the basic input and an optional attached label. Clicking the label, for a checkbox, checks the input. And they don't have to be next to each other, you can relate them with the id attribute of the input matching the for attribute of the label. So now I can have the action of clicking a thumbnail reach baaack in the DOM to the element just before the previous thumbnail image. So then:

  • input:checked + label is the Previous image.
  • input:checked + label + input + label is the Current image.
  • input:checked + label + input + label + input + label is the Next image.

Wrapping the label's image in a figure gives me a nicely semantic figcaption element. Normally I just style an em in the comment of a the markdown Image inclusion; but since I'm scattering HTML everywhere to make the DOM match what the CSS will search for I can't use markdown image tags anyway.

The two gotcha cases are the First and Last image. So for the First image I make a dummy input,label pair for the cascading CSS to match. For the Last image I do the same, but I also turn that into the big X close button for the whole shebang. Clicking that image turns the whole thing off because its label is directly attached to it, so it cascades nowhere important and hides itself.

As an added bonus, browsers that have keyboard navigation for Radios/ Selects etc. can use the keyboard to navigate the thumbnails. Winning!

<section class="gallery-2020-3">
<!-- Holds the gallery -->
    <input type="radio" name="gallery-2020-3" id="gallery-2020-3-0" />
    <!-- A Previous button placeholder for the first image. 
    So the CSS hooks onto something for previous -->
    <label></label>
    <input type="radio" name="gallery-2020-3" id="gallery-2020-3-1" /> 
    <!-- An image -->
    <label for="gallery-2020-3-0">
    <!-- Label to link this thumbnail with the _previous_ radio,
    so CSS cascade can find Previous, Current and Next images -->
        <figure>
            <img sizes="100vw" srcset="
                /blog/media/2020/05/undeadweightfinal.jpg.jpg 400w,
                /blog/media/2020/05/undeadweightfinal.jpg-228x300.jpg 228w,
                /blog/media/2020/05/undeadweightfinal.jpg-120x0-c-default.jpg 120w                
                " src="/blog/media/2020/05/undeadweightfinal.jpg-120x0-c-default.jpg"
                alt="Undead Weight" />
            <!-- srcset combines thumbnail and big images in a delightful package -->
            <figcaption>
            <!-- Semantic captioning! Also a link to download or visit a page -->
                <a href="/blog/media/2020/05/undeadweightfinal.jpg-120x0-c-default.jpg">
                    <em>Undead Weight</em>
                </a>
            </figcaption>
        </figure>
    </label>

    <!-- ... images repeat ... -->

    <input type="radio" name="gallery-2020-3" id="gallery-2020-3-close" />
    <!-- The Next button also functions as the Close hook for the last element! -->
    <label for="gallery-2020-3-close">
        X
    </label>
</section>
    .gallery-2020-3 {
        /* Gallery holder. Position relative to provide anchor, flex to fit */
        position: relative;
        width: 100%;
        display: flex;
    }

    .gallery-2020-3 label:last-child {
        /* Don't show that last image, it's a secret close image */
        display: none;
    }

    .gallery-2020-3 input {
        /* display:none inputs can't be interacted with, so shrink it _really_ small */
        transform: scaleX(0.00001);
    }

    .gallery-2020-3 label {
        /* Attractive thumbnails */
        height: 100px;
        width: 100px;
        max-width: var(--gallery-thumbnail-height);
        max-height: var(--gallery-thumbnail-height);
        /* the next three centre the image in the thumbnail l<->r and t<->b */
        display: flex;
        align-items: center;
        justify-content: center;
        background: url(/theme/images/Leather.png) repeat #1f0b02;
        /* stitches */
        border: 2px dashed #EADAC2;
        box-shadow: 0 0 0 4px #1f0b02, 2px 1px 6px 4px rgba(10, 10, 0, 0.5);
        border-radius: 10px;
    }

    .gallery-2020-3 label figure {
        /* Reinforce those sizes */
        width: var(--gallery-thumbnail-height);
        height: var(--gallery-thumbnail-height);
    }

    .gallery-2020-3 label img {
        /* _REINFORCE THOSE SIZES. Also display centred */
        display: inline;
        vertical-align: middle;
        max-width: var(--gallery-thumbnail-height);
        max-height: var(--gallery-thumbnail-height);
    }

    .gallery-2020-3 label img+figcaption {
        /* Hide caption for thumbnail */
        display: none;
    }

    .gallery-2020-3 input:checked+label {
        /** Previous button is the first Label after a checked input **/
        /** Put it on the left, over the image */
        position: fixed;
        left: 0;
        top: calc(50% - 50px);
        padding: auto 0;
        z-index: 3;
        display: flex;
        align-items: center;
        justify-content: center;
        background: url(/theme/images/Leather.png) repeat #1f0b02;
        border: 2px dashed #EADAC2;
        width: calc(100% - 12px);
        margin-left: 4px;
        box-shadow: 0 0 0 4px #1f0b02, 2px 1px 6px 4px rgba(10, 10, 0, 0.5);
        border-top-left-radius: 0;
        border-bottom-left-radius: 0;
        border-top-right-radius: 10px;
        border-bottom-right-radius: 10px;
    }

    .gallery-2020-3 input:checked+label+input+label {
        /** Focused image is the second label sibling from the selected input **/
        position: fixed;
        left: 0;
        top: 0;
        text-align: center;
        height: calc(100% - 20px);
        z-index: 2;
        background: white;
        padding: 0;
        margin: 10px;
        display: flex;
        align-items: center;
        justify-content: center;
        border: 2px dashed #EADAC2;
        width: calc(100% - 20px);
        max-width: initial;
        max-height: initial;
        margin-left: 4px;
        box-shadow: 0 0 0 4px #1f0b02, 2px 1px 6px 4px rgba(10, 10, 0, 0.5);
        border-radius: 10px;
        /** Beautiful cushiony background **/
        /** from https://leaverou.github.io/css3patterns/#upholstery */
        background:
            radial-gradient(hsl(0, 100%, 27%) 4%, hsl(0, 100%, 18%) 9%, hsla(0, 100%, 20%, 0) 9%) 0 0,
            radial-gradient(hsl(0, 100%, 27%) 4%, hsl(0, 100%, 18%) 8%, hsla(0, 100%, 20%, 0) 10%) 50px 50px,
            radial-gradient(hsla(0, 100%, 30%, 0.8) 20%, hsla(0, 100%, 20%, 0)) 50px 0,
            radial-gradient(hsla(0, 100%, 30%, 0.8) 20%, hsla(0, 100%, 20%, 0)) 0 50px,
            radial-gradient(hsla(0, 100%, 20%, 1) 35%, hsla(0, 100%, 20%, 0) 60%) 50px 0,
            radial-gradient(hsla(0, 100%, 20%, 1) 35%, hsla(0, 100%, 20%, 0) 60%) 100px 50px,
            radial-gradient(hsla(0, 100%, 15%, 0.7), hsla(0, 100%, 20%, 0)) 0 0,
            radial-gradient(hsla(0, 100%, 15%, 0.7), hsla(0, 100%, 20%, 0)) 50px 50px,
            linear-gradient(45deg, hsla(0, 100%, 20%, 0) 49%, hsla(0, 100%, 0%, 1) 50%, hsla(0, 100%, 20%, 0) 70%) 0 0,
            linear-gradient(-45deg, hsla(0, 100%, 20%, 0) 49%, hsla(0, 100%, 0%, 1) 50%, hsla(0, 100%, 20%, 0) 70%) 0 0;
        background-color: #300;
        background-size: 100px 100px;
    }

    .gallery-2020-3 input:checked+label+input+label>figure {
        /** Make sure it's big */
        width: 100%;
        height: 100%;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }

    .gallery-2020-3 input:checked+label+input+label>figure>img {
        /** Give it an image border, for niceness **/
        /** Max width is the Viewport Width - 20px for margin and 10 px / 2 for border */
        max-width: calc(100vw - 20px - 10px / 2);
        max-height: initial;
        padding: 2px;
        background: var(--paper-image);
    }

    .gallery-2020-3 input:checked+label+input+label>figure>figcaption {
        /** Match the caption background to the image border, and pretty **/
        background: var(--paper-image) rgba(255, 255, 0, 0.1);
        padding: 5px;
        border-radius: 10px;
        display: inherit;
    }

    .gallery-2020-3 input:checked+label+input+label+input+label {
        /** Next button, like the Previous button, third from the selected item **/
        position: fixed;
        top: calc(50% - 50px);
        right: 0;
        z-index: 3;
        display: flex;
        align-items: center;
        justify-content: center;
        background: url(/theme/images/Leather.png) repeat #1f0b02;
        border: 2px dashed #EADAC2;
        width: calc(100% - 12px);
        margin-left: 4px;
        box-shadow: 0 0 0 4px #1f0b02, 2px 1px 6px 4px rgba(10, 10, 0, 0.5);
        border-top-left-radius: 10px;
        border-bottom-left-radius: 10px;
        border-top-right-radius: 0;
        border-bottom-right-radius: 0;
    }

    .gallery-2020-3 input:checked+label+input+label+input+label:last-child {
        /** If Next is also Close, then over-ride the above with the close **/
        position: fixed;
        top: 0;
        right: 0;
        z-index: 3;
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        box-shadow: none;
        width: var(--gallery-thumbnail-height);
        height: var(--gallery-thumbnail-height);
        font-size: 95px;
        font-weight: bold;
    }

    .gallery-2020-3 input:checked~label:last-child {
        /** If Next is not also Close, close needs its own style **/
        position: fixed;
        top: 0;
        right: 0;
        z-index: 3;
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        box-shadow: none;
        width: var(--gallery-thumbnail-height);
        height: var(--gallery-thumbnail-height);
        font-size: 95px;
        font-weight: bold;
    }

    .gallery-2020-3 input:checked+label:last-child {
        /** If closed is selected then... just go away **/
        display: none;
    }

    .gallery-2020-3 input:first-child+label {
        display: none !important;
    }