Search code examples
htmlcsstwitter-bootstrapcss-selectorsradio-button

Can I affect an element outside the div I am currently in purely with CSS?


Context: I am attempting to use the radio hack to toggle what text is viewed within the .tabinfo div, but my radios and the text whose display attribute I want to change are located in different divs.

Problem: Is it possible to use pure CSS selectors to select the #text element by clicking on a nested radio?

Reference Code: I am using the bootstrap layout and have created the following HTML code:

<div class="col-xs-2">
    <input id="tab1" type="radio" name="tabs">
    <label for="tab1">Foo</label>
</div>
<div class="col-xs-2">
    <input id="tab2" type="radio" name="tabs">
    <label for="tab2">Bar</label>
</div>
<div class="col-xs-2">
    <input id="tab3" type="radio" name="tabs" checked> 
    <label for="tab3">Foo Bar</label>
</div>
<div class="col-xs-12">
    <div class="tabinfo">
        <div id="text1">
        </div>
        <div id="text2">
        </div>
        <div id="text3">
        </div>
    </div>
</div>

And the following CSS:

label {
    border: solid;
    border-top-right-radius: 10px;
    border-top-left-radius: 10px;
    border-bottom: none;
    border-color: rgb(211,211,205);
    border-width: 2px;
    color: rgb(12,174,175);
    background-color: rgb(247,247,247);
}

input:checked + label {
    background-color: #fff;
    color: rgb(94,94,94);
}

label:hover {
    cursor: pointer;
}

.tabinfo {
    border: solid;
    border-color: rgb(211,211,205);
    border-width: 2px;
    border-top-right-radius: 10px;
}

#tab1:checked ~ .col-xs-12 .tabinfo #text1,
#tab2:checked ~ .col-xs-12 .tabinfo #text2,
#tab3:checked ~ .col-xs-12 .tabinfo #text3 {
    display: block!important;
}

As you probably already guessed, the above does not work since the #texts and the #tabs are located in different divs. Is there any workaround or any solution without breaking the Bootstrap layout?


Solution

  • A brittle solution can be used, but this involves moving the <input> elements away from the <label> elements, and you specify one requirement of any HTML changes is that any change

    …does not break the [Bootstrap] layout.

    I don't think my changes break that layout, but I'm not entirely sure, so you will need to evaluate this yourself.

    That preamble aside, however, I've modified your HTML to the following:

    <input id="tab1" type="radio" name="tabs" />
    <input id="tab2" type="radio" name="tabs" />
    <input id="tab3" type="radio" name="tabs" />
    <div class="col-xs-2">
      <label for="tab1">Foo</label>
    </div>
    <div class="col-xs-2">
      <label for="tab2">Bar</label>
    </div>
    <div class="col-xs-2">
      <label for="tab3">Foo Bar</label>
    </div>
    <div class="col-xs-12">
      <div class="tabinfo">
        <div id="text1">
        </div>
        <div id="text2">
        </div>
        <div id="text3">
        </div>
      </div>
    </div>
    

    This approach allows us to take advantage of the <label> element's ability to check/uncheck its associated <input> element regardless of where in the document it may be located (so long as the for attribute identifies the id of that associated <input>); placing the <input> elements ahead of the content allows us to use sibling combinators to find the elements containing the relevant content to style.

    On the assumption that you wish to retain the visual effect of the <input> being checked, or otherwise, we've also used CSS generated content to emulate a checked or unchecked radio; this could use some fine tuning, though:

    /* Here we hide all <div> elements within the .tabinfo
       element, and also all <input> elements whose 'name'
       attribute is equal to 'tabs' and whose 'type' is
       equal to 'radio': */
    
    .tabinfo div,
    input[name=tabs][type=radio] {
      display: none;
    }
    
    
    /* This styles the generated content of the ::before
       pseudo-element to show the attribute-value of the
       element's 'id' attribute; purely for the purposes
       of this demo: */
    
    div[id^=text]::before {
      content: attr(id);
    }
    
    
    /* Styling the generated content, the ::before pseudo-
       element, of the <label> elements, in order to
       emulate the moved radio <input>: */
    
    label::before {
      /* An empty string, content is required in order for
         the pseudo-element to be visible on the page: */
      content: '';
      /* To allow the pseudo-element to have specified
         width and height values: */
      display: inline-block;
      height: 1em;
      width: 1em;
      /* To include the border, and any padding, widths
         in the calculations for the element's size: */
      box-sizing: border-box;
      background-color: #eee;
      border: 1px solid #999;
    
      /* In order for the pseudo-radio to have a round
         shape/border: */
      border-radius: 50%;
      margin-right: 0.2em;
    }
    
    /* This selector styles the <label> element whose 'for'
       attribute is equal to 'tab1', which is a child of
       the div.col-xs-2 element which itself is a general
       sibling of the #tab1 element when that element is
       checked; this is the 'checked' style of the pseudo-
       'radio' generated content: */
    #tab1:checked~div.col-xs-2>label[for=tab1]::before {
      background-color: #666;
      box-shadow: inset 0 0 0 3px #fff;
    }
    
    /* This selects the element with an id of 'text1',
       inside of a <div> with the class of 'col-xs-12',
       which is a general sibling of the '#tab1' element
       when that element is checked: */
    #tab1:checked~div.col-xs-12 #text1 {
    
      /* Here we make the content of that element visible: */
      display: block;
    }
    
    #tab2:checked~div.col-xs-2>label[for=tab2]::before {
      background-color: #666;
      box-shadow: inset 0 0 0 3px #fff;
    }
    
    #tab2:checked~div.col-xs-12 #text2 {
      display: block;
    }
    
    #tab3:checked~div.col-xs-2>label[for=tab3]::before {
      background-color: #666;
      box-shadow: inset 0 0 0 3px #fff;
    }
    
    #tab3:checked~div.col-xs-12 #text3 {
      display: block;
    }
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
    
    <input id="tab1" type="radio" name="tabs" />
    <input id="tab2" type="radio" name="tabs" />
    <input id="tab3" type="radio" name="tabs" />
    <div class="col-xs-2">
      <label for="tab1">Foo</label>
    </div>
    <div class="col-xs-2">
      <label for="tab2">Bar</label>
    </div>
    <div class="col-xs-2">
      <label for="tab3">Foo Bar</label>
    </div>
    <div class="col-xs-12">
      <div class="tabinfo">
        <div id="text1">
        </div>
        <div id="text2">
        </div>
        <div id="text3">
        </div>
      </div>
    </div>

    JS Fiddle demo.

    As you can see from the rules formed to show the elements related to the checked <input> elements those rules require some precision and repetition, since CSS has no concept of this, so, given a data-affectedby attribute whose value might be set to the id of the related <input>, there's no way we can have a rule along the lines of:

    input[id^=tab]:checked ~ .col-xs-12 [data-affectedby=this.id]