1

I'm working on an app where I am using the DOMTokenList for shown and hidden an element using add() and remove() methods but the code is getting big. Example:

button.addEventListener("click", () => {
   elem1.classList.add("display-none");
   elem2.classList.remove("display-none");
   elem3.classList.remove("display-none");
   elem4.classList.add("display-none");
});

button2.addEventListener("click", () => {
   elem1.classList.remove("display-none");
   elem2.classList.add("display-none");
   elem3.classList.add("display-none");
   elem4.classList.remove("display-none");
});

I've seven code parts similar to the example and I started looking others ways but I tried with CSS

.box {
    --displayRed: block;
    --displayGreen: none;
    --displayBlue: none;
    height: 100px;
    width: 100px;
}

.box--red { 
    display: var(--displayRed);
    background-color: #ff0000;
}

.box--green { 
    display: var(--displayGreen);
    background-color: #00ff00;
}

.box--blue { 
    display: var(--displayBlue);
    background-color: #0000ff;
}

.box--red:target {
  --displayRed: block;
  --displayGreen: none;
  --displayBlue: none;
}

.box--green:target {
  --displayRed: none;
  --displayGreen: block;
  --displayBlue: none;
}

.box--blue:target {
  --displayRed: none;
  --displayGreen: none;
  --displayBlue: block;
}
<div>
    <div class="box">
        <div id="boxRed" class="box box--red"></div>
        <div id="boxGreen" class="box box--green"></div>
        <div id="boxBlue" class="box box--blue"></div>
    </div>
    
    <nav>
      <ul>
        <li><a href="#boxRed">Show red box</a></li>
        <li><a href="#boxGreen">Show green box</a></li>
        <li><a href="#boxBlue">Show blue box</a></li>
      </ul>
    </nav>
</div>

I was expecting the red box to hide when I clicked the link to show the yellow box but it only scrolled down and didn't hide the red box. I tried to find others solution with videos, Pseudo-classes, Selectors, even with two question that are Can text be hidden and shown using just CSS (no JavaScript code)? [closed] and Ocultar y Mostrar un elemento css but I can't find the perfect idea. I ACCEPT SUGGESTIONS

dippas
  • 58,591
  • 15
  • 114
  • 126
MART3
  • 31
  • 6
  • 1
    you can use css only, with `:hover` or `:focus` or another selector in css. – Gilang Pratama Jul 08 '22 at 10:02
  • 1
    I may be wrong but I think it's only possible without js if you don't have any of you boxes shown by default since you would need to remove a display-block or remove a display-none class. If hiding every div by default fits with your needs, you can check [this answer](https://stackoverflow.com/a/72885829/17684809) – Cédric Jul 08 '22 at 10:06
  • @GilangPratama OK thanks, I'll think about the selectors – MART3 Jul 08 '22 at 22:04
  • @Cédric Yep, the idea it's good but I need an element to be displayed by default – MART3 Jul 08 '22 at 22:13
  • @dippas OP already knew the `:target` solution, their problem was further down the rabbit hole. I'll reopen this question but if you find a better dupe please ping me so that I can close it again. – Kaiido Jul 09 '22 at 14:17
  • @Kaiido a dupe? I am new here :c – MART3 Jul 10 '22 at 01:29
  • @MART3 a duplicate question, one that asks for the same thing. User dippas got confused and thought your question had already been asked and thus closed it as a "duplicate". However the one they found was actually not about the same issue (even though a little bit related). So I reopened your question (both them and I can do this single-handedly because we have "gold-badges" in the question's tags: css or html). However me reopening the question means they can't change the dupe target anymore, but I still can. So if they find a better target I still can close it, but I doubt there is. – Kaiido Jul 10 '22 at 02:22
  • @Kaiido if it helps you, my question is about the element if it could be hidden or shown without js but the only problem is that the displayed item loses state when another link is clicked. Let me know if I can help you with anything else on my twitter [@goddmartee](https://twitter.com/goddmartee). – MART3 Jul 10 '22 at 03:32

3 Answers3

4

Since your goal is to hide the elements with display:none, I'll assume the DOM order doesn't matter.

So what you can do with no JS is to place your default element at the end of the container and then hide it when it's following a :targeted element:

.box {
    --displayRed: block;
    --displayGreen: none;
    --displayBlue: none;
    height: 100px;
    width: 100px;
}

.box--red { 
    display: var(--displayRed);
    background-color: #ff0000;
}

.box--green { 
    display: var(--displayGreen);
    background-color: #00ff00;
}

.box--blue { 
    display: var(--displayBlue);
    background-color: #0000ff;
}

:target ~ .box--red { /* when any other than red is selected */
  --displayRed: none;
}
.box--green:target {
  --displayRed: none;
  --displayGreen: block;
  --displayBlue: none;
}

.box--blue:target {
  --displayRed: none;
  --displayGreen: none;
  --displayBlue: block;
}
<div>
    <div class="box">
        <div id="boxGreen" class="box box--green"></div>
        <div id="boxBlue" class="box box--blue"></div>
        <!-- move the default one last -->
        <div id="boxRed" class="box box--red"></div>
    </div>
    
    <nav>
      <ul>
        <li><a href="#boxRed">Show red box</a></li>
        <li><a href="#boxGreen">Show green box</a></li>
        <li><a href="#boxBlue">Show blue box</a></li>
      </ul>
    </nav>
</div>

Ps: as noted by T.J Crowder in the comments below, the css-variables in here are quite useless as they won't actually reach the elements they're supposed to affect.

So you could rewrite all this without these variables and simply do:

.box {
    height: 100px;
    width: 100px;
}
/* might be better to remove the "box" class on the container but... */
.box[id] {
    display: none;
}
.box:target ~ .box--red {
    display: none;
}
.box:target {
    display: block;
}
/* This one is the default one */
.box.box--red { 
    background-color: #ff0000;
    display: block;
}
/* only the color needs to be defined for the others */
.box--green { 
    background-color: #00ff00;
}

.box--blue {
    background-color: #0000ff;
}
<div>
    <div class="box">
        <div id="boxGreen" class="box box--green"></div>
        <div id="boxBlue" class="box box--blue"></div>
        <!-- move the default one last -->
        <div id="boxRed" class="box box--red"></div>
    </div>
    
    <nav>
      <ul>
        <li><a href="#boxRed">Show red box</a></li>
        <li><a href="#boxGreen">Show green box</a></li>
        <li><a href="#boxBlue">Show blue box</a></li>
      </ul>
    </nav>
</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 1
    That general combinator trick is really nice! But the CSS variables aren't doing anything useful in the above, are they? In fact, the way they're defined (inherited from the question) seems misleading...? `.box--blue:target { --displayRed: none; }` doesn't affect `--displayRed` in `.box--red`, just in `.box--blue:target`. ([Example](https://jsfiddle.net/tjcrowder/btLwhau1/).) You can take the above and change it to simply defaulting to hiding the green and blue then overriding that in their `:target` selectors, without any variables: https://jsfiddle.net/tjcrowder/gp31rhq7/ (cc @MART3) – T.J. Crowder Jul 09 '22 at 09:14
  • 2
    @T.J.Crowder you're entirely right, you can even simplify it a bit more by not repeating the `.color1:target, .color2:target, .colorN:target { display: block; }` and instead hide them all based on their base class, and show any `:target` as shown in my edit. – Kaiido Jul 09 '22 at 14:15
3

The trick is defaulting to the red box when there's no hash fragment. I'm not sure you can do that with a pure CSS solution. (Edit: it turns out you can, as shown by Kaiido, provided you can put the default at the end; I should have thought of the general sibling combinator! But the CSS custom properties [aka "variables"] aren't actually doing anything in that AFAICS, see this fiddle.)

I don't know if it's possible to do this with CSS custom properties (aka "variables"); I tend to think not, since setting a custom property in the selector rule for one sibling (say, .box--blue:target) doesn't change its value for any elements not inside that .box--blue element (doesn't change it for the .box--red sibling, for instance). (Fiddle.) But you can definitely show/hide boxes with the old checkbox/radio button trick:

  1. Define the boxes as display: none.

  2. Have invisible radio buttons immediately prior to the box the radio button will relate to in the same parent:

    <input type="radio" name="box-controller" id="chk-box--red" checked>
    <div id="boxRed" class="box box--red"></div>
    
  3. Have label elements that tick the radio button for that box (via id/for) instead of links.

    <label tab-index="0" for="chk-box--red">Show red box</label>
    

    In the above, note that I've added tab-index to the label so it show sup in the tabbing order. We probably also want CSS that underlines it or similar.

  4. Have a CSS rule that says the .box immediately after a checked radio button should be display: block:

    input[name=box-controller]:checked + .box {
        display: block;
    }
    

Live example:

.box {
    height: 100px;
    width: 100px;
}
/* Hide boxes inside the outer box until/unless shown */
.box .box {
    display: none;
}

label {
    text-decoration: underline;
}

input[name=box-controller] {
    display: none;
}
input[name=box-controller]:checked + .box {
    display: block;
}

.box--red { 
    background-color: #ff0000;
}

.box--green { 
    background-color: #00ff00;
}

.box--blue { 
    background-color: #0000ff;
}
<div>
    <div class="box">
        <input type="radio" name="box-controller" id="chk-box--red" checked>
        <div id="boxRed" class="box box--red"></div>
        <input type="radio" name="box-controller" id="chk-box--green">
        <div id="boxGreen" class="box box--green"></div>
        <input type="radio" name="box-controller" id="chk-box--blue">
        <div id="boxBlue" class="box box--blue"></div>
    </div>
    
    <nav>
      <ul>
        <li><label tab-index"0" for="chk-box--red">Show red box</label></li>
        <li><label tab-index"0" for="chk-box--green">Show green box</label></li>
        <li><label tab-index"0" for="chk-box--blue">Show blue box</label></li>
      </ul>
    </nav>
</div>

Important caveat: With the above, the URL is no longer driving the process (it doesn't change as you choose boxes). That's because you can't have a label inside an a, and if you put the a inside the label because it prevents the label doing its job. If having the state in the URL is important to you, look at the JavaScript solution (which doesn't repeat itself) below.


You indicated that your concern with the JavaScript solution was that you were repeating a lot of code. There's no need to do that. Just for what it's worth, here's a JavaScript solution driven the hash fragment that works regardless of how many boxes you have; comments within:

// The function that shows the current box, hiding others
function showCurrentBox() {
    // Get the box to show, defaulting to `boxRed`
    const hash = location.hash.replace(/^#/, "") || "boxRed";

    // Hide any box we've previously shown
    // (Note the use of optional chaining, since `querySelector` may return `null`)
    document.querySelector(".box.showing")?.classList.remove("showing");

    // Show the box (again with optional chaining in case the fragment doesn't identify a box)
    document.getElementById(hash)?.classList.add("showing");
}
// Run on startup
showCurrentBox();
// Run whenver the hash changes
window.addEventListener("hashchange", showCurrentBox);
.box {
    height: 100px;
    width: 100px;
}

/* Hide boxes inside the outer box until/unless shown */
.box .box {
    display: none;
}

.box.showing {
    display: block;
}

.box--red { 
    background-color: #ff0000;
}

.box--green { 
    background-color: #00ff00;
}

.box--blue { 
    background-color: #0000ff;
}
<div>
    <div class="box">
        <div id="boxRed" class="box box--red showing"></div>
        <div id="boxGreen" class="box box--green"></div>
        <div id="boxBlue" class="box box--blue"></div>
    </div>
    
    <nav>
      <ul>
        <li><a href="#boxRed">Show red box</a></li>
        <li><a href="#boxGreen">Show green box</a></li>
        <li><a href="#boxBlue">Show blue box</a></li>
      </ul>
    </nav>
</div>

Adding boxes to that is just a matter of defining their CSS and giving them an ID; the JavaScript code doesn't change.

With this solution, the URL does drive the boxes. Bookmark the URL showing the green box, and that's what shows when you come back to it.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
1

You can keep things DRY with JavaScript by caching all the boxes first, and using data attributes instead of ids. Initialise all the boxes as display: none.

Add one event listener to the ul element (using event delegation to watch for events from its child elements as they "bubble up" the DOM), switch off all the boxes by looping over the boxes and removing a show class, and then adding a show class (display: block;) to the element that corresponds to the data-id of the button that was clicked.

You can add as many boxes/buttons as you want as you can see in this example.

// Cache the boxes, and the list element, and attach
// one listener to the list element
const boxes = document.querySelectorAll('.box');
const list = document.querySelector('ul');
list.addEventListener('click', handleClick);

function handleClick(e) {

  // If the child element that fired the event
  // is a button
  if (e.target.matches('button')) {

    // Destructure the id from its dataset
    const { id } = e.target.dataset;

    // Iterate over all the boxes removing the show class
    boxes.forEach(box => box.classList.remove('show'));

    // Grab the box which has a class that corresponds
    // to the id: `.red` for example
    const current = document.querySelector(`.${id}`);

    // And then show that box
    current.classList.add('show');
  }
}
.box { display: none; padding: 0.3em; color: white; }
.show { display: block; }
.red { background-color: red; }
.green { background-color: green; }
.blue { background-color: blue; }
.orange { background-color: orange; }
.black { background-color: black; }
ul { margin-left: 0; padding: 0; }
li { display: inline; }
button:hover { cursor: pointer; background-color: #fffff0; }
<div>
  <nav>
    <ul>
      <li><button data-id="red">Red</button></li>
      <li><button data-id="green">Green</button></li>
      <li><button data-id="blue">Blue</button></li>
      <li><button data-id="orange">Orange</button></li>
      <li><button data-id="black">Black</button></li>
    </ul>
  </nav>
  <div class="boxes">
    <div class="box red">Red</div>
    <div class="box green">Green</div>
    <div class="box blue">Blue</div>
    <div class="box orange">Orange</div>
    <div class="box black">Black</div>
  </div>
</div>

Additional documentation

Andy
  • 61,948
  • 13
  • 68
  • 95