18

The following code use a script to toggle/uncheck a radio when clicked a second time on the same.

My question is how do I do this using CSS only?

(function(lastimg) {
  document.querySelector("#img-select").addEventListener('click', function(e){
    if (e.target.tagName.toLowerCase() == 'input') {
      if (lastimg == e.target) {
        e.target.checked = false;
        lastimg = null;
      } else {
        lastimg = e.target;
      }      
    }
  });
}());
.container {
  display: flex;
  flex-wrap: wrap;
  max-width: 660px;
}
.container > label {
  flex: 1;
  flex-basis: 33.333%;
}
.container > div {
  flex: 1;
  flex-basis: 100%;
}
.container label img {
  display: block;
  margin: 0 auto;
}
.container input, .container input ~ div {
  display: none;
  padding: 10px;
}

.container #img1:checked ~ #img1txt,
.container #img2:checked ~ #img2txt,
.container #img3:checked ~ #img3txt,
.container #img4:checked ~ #img4txt {
  display: block;
}
<div id="img-select" class="container">
  <input id="img1" type="radio" name="img-descr">
  <input id="img2" type="radio" name="img-descr">
  <input id="img3" type="radio" name="img-descr">

  <label for="img1">
    <img src="http://lorempixel.com/200/200/food/1/" alt="">
  </label>
  <label for="img2">
    <img src="http://lorempixel.com/200/200/food/6/" alt="">
  </label>
  <label for="img3">
    <img src="http://lorempixel.com/200/200/food/8/" alt="">
  </label>

  <div id="img1txt">
    <div>Recipe nr 1</div>
  </div>
  <div id="img2txt">
    <div>Recipe nr 2</div>
  </div>
  <div id="img3txt">
    <div>Recipe nr 3</div>
  </div>
</div>

Edit

I need a cross browser solution, working on the major browsers, and without script, just CSS.

To clarify, I want it to work as a normal radio input, but if clicked twice, or repeatedly, on the same, it should toggle itself as a checkbox input does.

Also markup change are allowed, as long as the layout structure is kept the same, and I would also prefer if it can break line, as a page can have more than 3 recipes.

Edit 2

The main focus of the question is how to make a radio input togglable, though since a couple of answers show other ways to toggle a state with pure CSS, any such tricks is welcome.

Asons
  • 84,923
  • 12
  • 110
  • 165
  • 1
    One question for the text position, all at bottom left corner or under each image? – Stickers Mar 25 '16 at 14:48
  • 1
    @Pangloss How the text is displayed is of less concern, the toggling solution is the major goal. – Asons Mar 25 '16 at 14:49
  • 1
    OK, the reason is the markup can be quite different for different text displays. – Stickers Mar 25 '16 at 14:51
  • 1
    @Pangloss It can, yes, so I am more after the logic here, which tricks can be pulled to achieve a togglable radio input – Asons Mar 25 '16 at 14:55
  • 1
    You are doing something wrong. The user experience you explain is a checkbox user experience. Why dont you use checkbox and insist on using radio? – cenk ebret Mar 31 '16 at 13:32
  • 1
    @cenkebret: I need a solution where an input should work both as a radio and a checkbox, where only one item can be selected/checked at a time and if one click the same it should toggle checked/unchecked, ... and that will not work if I would pick checkbox instead. – Asons Mar 31 '16 at 13:45

6 Answers6

18

You can't change the functionality of radio buttons using CSS. CSS is designed for visual changes only.

That said, you can simulate this behavior with a clever hack. For your example, I'd recommend using CSS to visually replace the label for the currently selected radio button with a dummy label attached to another radio button representing a "blank" or "empty" selection. That way, clicking the dummy label would select the "blank" option, effectively clearing your prior choice:

.container {
  display: flex;
  flex-wrap: wrap;
  max-width: 660px;
}
.container > label {
  flex: 1;
  flex-basis: 33.333%;
}
.container > div {
  flex: 1;
  flex-basis: 100%;
}
.container label img {
  display: block;
  margin: 0 auto;
}
.container input, .container input ~ div {
  display: none;
  padding: 10px;
}

.container #img1:checked ~ #img1txt,
.container #img2:checked ~ #img2txt,
.container #img3:checked ~ #img3txt {
  display: block;
}

.container label[for=noimg] {
  display: none;
}

.container #img1:checked ~ label[for=img1],
.container #img2:checked ~ label[for=img2],
.container #img3:checked ~ label[for=img3] {
  display: none;
}

.container #img1:checked ~ label[for=img1] + label[for=noimg],
.container #img2:checked ~ label[for=img2] + label[for=noimg],
.container #img3:checked ~ label[for=img3] + label[for=noimg] {
  display: block;
}
<div id="img-select" class="container">
  <input id="noimg" type="radio" name="img-descr">
  <input id="img1" type="radio" name="img-descr">
  <input id="img2" type="radio" name="img-descr">
  <input id="img3" type="radio" name="img-descr">

  <label for="img1">
    <img src="http://lorempixel.com/200/200/food/1/" alt="">
  </label>
  <label for="noimg">
    <img src="http://lorempixel.com/200/200/food/1/" alt="">
  </label>
  <label for="img2">
    <img src="http://lorempixel.com/200/200/food/6/" alt="">
  </label>
  <label for="noimg">
    <img src="http://lorempixel.com/200/200/food/6/" alt="">
  </label>
  <label for="img3">
    <img src="http://lorempixel.com/200/200/food/8/" alt="">
  </label>
  <label for="noimg">
    <img src="http://lorempixel.com/200/200/food/8/" alt="">
  </label>

  <div id="img1txt">
    <div>Recipe nr 1</div>
  </div>
  <div id="img2txt">
    <div>Recipe nr 2</div>
  </div>
  <div id="img3txt">
    <div>Recipe nr 3</div>
  </div>
</div>

(View in JSFiddle)

Ajedi32
  • 45,670
  • 22
  • 127
  • 172
  • 1
    Thanks, accepted, as even if it has an extra image (which easily can be removed using an extra wrapper) it works no matter the images are in a square or randomly positioned, which quite often is the case. – Asons Mar 31 '16 at 19:08
6

If the effect does not need to be persistent, you can achieve something similar playing with :focus instead of using radio buttons.

To make an element focusable, set the tabindex attribute to an integer. Use a negative one if you don't want the element to be reached via sequential focus navigation (pressing the "tab" key).

.container {
  display: flex;
  flex-wrap: wrap;
  max-width: 660px;
}
.container > .img {
  flex: 1;
  position: relative;
}
.container > .img > .unselect {
  display: none;
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
}
.container > .txt {
  display: none;
  order: 1;
  flex-basis: 100%;
}
.container > .img:focus > .unselect,
.container > .img:focus + .txt {
  display: block;
}
<div id="img-select" class="container">
  <div class="img" tabindex="0">
    <img src="http://lorempixel.com/200/200/food/1/" alt="">
    <span class="unselect" tabindex="-1"></span>
  </div>
  <div class="txt">Recipe nr 1</div>
  <div class="img" tabindex="0">
    <img src="http://lorempixel.com/200/200/food/6/" alt="">
    <span class="unselect" tabindex="-1"></span>
  </div>
  <div class="txt">Recipe nr 2</div>
  <div class="img" tabindex="0">
    <img src="http://lorempixel.com/200/200/food/8/" alt="">
    <span class="unselect" tabindex="-1"></span>
  </div>
  <div class="txt">Recipe nr 3</div>
</div>
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • 1
    This trick rocks, I use focus frequently but not for this purpose, so thank you for posting it, though in this case I need it to persist so can't pick it. – Asons Mar 31 '16 at 18:50
5

Bounty Challenge Accepted (without the extra noimg)

.container {
    display: flex;
    flex-wrap: wrap;
    max-width: 660px;
    overflow: hidden;
    position: relative;
}
.container img {
    user-select: none;
    pointer-events: none;
}
.container > label {
    flex: 1;
    flex-basis: 33.333%;
    z-index: 1;
}
.container > div {
    flex: 1;
    flex-basis: 100%;
}
.container label img { margin: 0 auto }
.container input,
.container input ~ div {
    display: none;
    padding: 10px;
}
.container label[for=none] {
    position: absolute;
    bottom: 0;
    top: 0;
    left: 0;
    right: 0;
    z-index: 0;
}
.container #img1:checked ~ label[for=img1],
.container #img2:checked ~ label[for=img2],
.container #img3:checked ~ label[for=img3] {
    pointer-events: none;
    z-index: -1;
}
.container #img1:checked ~ #img1txt,
.container #img2:checked ~ #img2txt,
.container #img3:checked ~ #img3txt { display: block }
<div id="img-select" class="container">
  <input id="img1" type="radio" name="img-descr">
  <input id="img2" type="radio" name="img-descr">
  <input id="img3" type="radio" name="img-descr">

  <!-- Experimental -->
  <input id="none" type="radio" name="img-descr" checked>
  <label for="none"></label>

  <label for="img1">
    <img src="http://dummyimage.com/200/333" alt="">
  </label>
  <label for="img2">
    <img src="http://dummyimage.com/200/666" alt="">
  </label>
  <label for="img3">
    <img src="http://dummyimage.com/200/999" alt="">
  </label>

  <div id="img1txt">
    <div>Recipe nr 1</div>
  </div>
  <div id="img2txt">
    <div>Recipe nr 2</div>
  </div>
  <div id="img3txt">
    <div>Recipe nr 3</div>
  </div>
</div>

EDIT: By definition radio buttons shouldn't be toggleable (I forgot that this was an additional requirement in this task = to broke the rules). @Ajedi32 answer is probably the best, but it can be optimized (repeated images)? Bounty still in game...

EDIT 2: Now it's fully functional solution. (doing this trick https://stackoverflow.com/a/7392038/2601031)

EDIT 3: Multi-layer layout + Repaired selection.

Community
  • 1
  • 1
Maciej A. Czyzewski
  • 1,539
  • 1
  • 13
  • 24
  • 1
    Thanks, though you must have misread the question, it should toggle between checked/unchecked if one click on the same image repeatedly, which yours doesn't. – Asons Mar 27 '16 at 22:08
  • 1
    IMHO, to make a radio input togglable is not to break it, it is to extend it, and if you ask me, an input of type "radiocheckbox" would be very useful. – Asons Mar 28 '16 at 09:03
  • 1
    @LGSon what do you thing about **EDIT 2**? – Maciej A. Czyzewski Mar 29 '16 at 13:34
  • It's not allowed to nest labels inside labels, and even if it does work in this case, I can't accept that as a valid solution. – Asons Mar 29 '16 at 14:15
  • Minor thing I noticed with this solution is that double-clicking a box causes it to become selected (like text) and makes clicking other boxes no longer work until the selection is cleared. You might be able to fix that with `user-select`. It's also worth noting (as LGSon said), that this isn't _technically_ valid HTML (you can't put labels inside other labels), even if it does work in most browsers. – Ajedi32 Mar 29 '16 at 14:21
  • @Ajedi32 Do you know why it becomes selected like that? .. Is it because of the `pointer-events`? – Asons Mar 29 '16 at 14:56
  • @LGSon Double clicking text in a browser causes a word to become selected. (Try it on this comment.) Triple clicking selects the whole paragraph. I think the reason it's happening in this example but not the others is because the second click in the double click happens on the `none` label itself (because of pointer-events), not on an image as in other examples. – Ajedi32 Mar 29 '16 at 15:49
  • @Ajedi32 how would you rate this version? Now it's valid HTML... selection problems solved. (your solution has problems with selection) – Maciej A. Czyzewski Mar 29 '16 at 16:29
  • @MaciejA.Czyzewski Seems to work pretty well. Selections still occur on double-click (upon closer examination it looks like that's because user-select [is non-standard and requires prefixes](https://developer.mozilla.org/en-US/docs/Web/CSS/user-select)) but that no longer interferes with the operation of the system. One other minor consequence of this implementation I just noticed is that clicking the background near the images also clears the selection. That may or may not be desirable behavior depending on the exact use case here. – Ajedi32 Mar 29 '16 at 16:49
  • 2
    @Ajedi32 The clickable area out side the image relates to the label being bigger than the image, no big deal and an easy fix. I think this one beat yours :) – Asons Mar 29 '16 at 17:01
  • @LGSon Well, not just the individual labels being bigger than the image, but the `none` label too, which encompases all three images. Even if the label sizes were fixed to closely match the images, clicking the space between each image would still clear the selection. You're correct though, the basic principle is sound and this should be more-or-less easy to fix. (Assuming it even needs to be fixed in the first place.) – Ajedi32 Mar 29 '16 at 17:29
  • 2
    Thanks for your solution, you'll get the bounty, though I will accept another as this has one flaw, as soon as the clickable images no longer resides in a square, it becomes trickier to position the `none` label (sometimes not at all). – Asons Mar 31 '16 at 19:05
2

The answer is you can't unselect or uncheck a radio button in CSS only, as the radio button only becomes unchecked once you click on a different radio button. As only one radio button can be active at once, this will uncheck the previously checked radio button.

input:checked + label {
    color: green;
}

input:not(:checked) + label {
    color: red;
}

So you'll have to stick with using the JS function you posted.

Here are a couple of nice articles with further explanation :

CSS Click Events

How To Generate CSS Click Events

aphextwix
  • 1,838
  • 3
  • 21
  • 27
  • 2
    [That latter article](http://vanseodesign.com/css/click-events/) makes some good points about whether CSS hacks like the ones in these answers should actually be used. There are some questions about the maintainability and modularity of these methods vs just using JavaScript. Definitely worth a read. – Ajedi32 Mar 29 '16 at 16:56
2

The trick is using :target. I added two empty <a> tags in each block, and set them to cover the block completely in order to perform the click event. The first link is for the real :target event, and second link is just for undo it, with a bit help of z-index to make it happen.

jsFiddle

ul {
  padding: 0;
  margin: 0;
}
li {
  display: inline-block;
  vertical-align: top;
  position: relative;
}
a {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
}
div {
  display: none;
}
.target{
  z-index: 1;
}
.target:target {
  z-index: -1;
}
.target:target ~ div{
  display: block;
}
<ul>
  <li>
    <a href="#link-1" id="link-1" class="target"></a>
    <a href="#"></a>
    <img src="//dummyimage.com/150/333">
    <div>a</div>
  </li>
  <li>
    <a href="#link-2" id="link-2" class="target"></a>
    <a href="#"></a>
    <img src="//dummyimage.com/150/666">
    <div>b</div>
  </li>
  <li>
    <a href="#link-3" id="link-3" class="target"></a>
    <a href="#"></a>
    <img src="//dummyimage.com/150/999">
    <div>c</div>
  </li>
</ul>
Stickers
  • 75,527
  • 23
  • 147
  • 186
  • 2
    Side note, you've probably heard that `:target` can make page to jump, there is an excellent article from [CSS-Tricks](https://css-tricks.com/on-target/) talking about it. – Stickers Mar 25 '16 at 15:23
  • 1
    It's also worth noting that this also causes the URL of the page to update to include the hash fragment of the currently selected link. (And that, consequently, you can't use more than one of these on the same page.) You might be able to work around that by putting the links in iframes I suppose... – Ajedi32 Mar 28 '16 at 19:37
  • 1
    @Ajedi32 It's a good point, also in my case I already use `:target` in some situations to enable other elements on page load, for which this works perfect, so I can't use it for handling state as well ... otherwise this is a simple and effective alternative to radio inputs (when a persistent state is needed). – Asons Mar 29 '16 at 07:54
1

I have a solution which is halfway through:

  • It clears selection on double click but not on click (as requested)
  • But (and that's the "halfway"), it will not clear selection when double clicking the currently selected item (only when double clicking other items)

 .container {
            display: flex;
            flex-wrap: wrap;
            max-width: 660px;
            overflow: hidden;
            position: relative;
        }

        .container img {
            user-select: none;
            pointer-events: none;
        }

        .container > label {
            flex: 1;
            flex-basis: 33.333%;
        }

        .container > div {
            flex: 1;
            flex-basis: 100%;
        }

        .container label img {
            margin: 0 auto
        }

        .container input,
        .container input ~ div {
            display: none;
            padding: 10px;
        }

        .container label[for=none] {
            position: absolute;
            width: 200px;
            height: 200px;
            z-index: 1;
            display: none;
        }

        .container #img1:checked ~ label[for=none],
        .container #img2:checked ~ label[for=none],
        .container #img3:checked ~ label[for=none] {
            display: block;
        }

        @keyframes hideNone {
            from {
                z-index: 3;
            }
            to {
                z-index: 3;
            }
        }

        .container #img1:checked ~ label[for=img1],
        .container #img2:checked ~ label[for=img2],
        .container #img3:checked ~ label[for=img3] {
            animation-name: hideNone;
            animation-delay: 0.3s;
            animation-duration: 99999s;
        }

        .container #img1:checked ~ label[for=none] {
            left: 0;
        }

        .container #img2:checked ~ label[for=none] {
            left: 220px;
        }

        .container #img3:checked ~ label[for=none] {
            left: 440px;
        }

        .container #img1:checked ~ #img1txt,
        .container #img2:checked ~ #img2txt,
        .container #img3:checked ~ #img3txt {
            display: block
        }
<div id="img-select" class="container">
    <input id="img1" type="radio" name="img-descr">
    <input id="img2" type="radio" name="img-descr">
    <input id="img3" type="radio" name="img-descr">

    <input id="none" type="radio" name="img-descr" checked>
    <label for="none"></label>

    <input type="button" autofocus>
    <label for="img1">
        <img src="http://dummyimage.com/200/333" alt="">
    </label>
    <label for="img2">
        <img src="http://dummyimage.com/200/666" alt="">
    </label>
    <label for="img3">
        <img src="http://dummyimage.com/200/999" alt="">
    </label>

    <div id="img1txt">
        <div>Recipe nr 1</div>
    </div>
    <div id="img2txt">
        <div>Recipe nr 2</div>
    </div>
    <div id="img3txt">
        <div>Recipe nr 3</div>
    </div>
</div>

Still thinking on how to improve it... :)

Yoav Aharoni
  • 2,672
  • 13
  • 18