nestedtailwind-csspeer

How can the peer class be used not only on sibling elements but also within nested child elements?


The style for the label element itself applies but for the styled radio button it doesn't. as far as I could troubleshoot it's because the peer class can't be directly applied to nested elements but I believe there must be some workaround.

Can't get this to work:

https://play.tailwindcss.com/q2CNd9zkjp

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

<fieldset class="space-y-2">
  <legend class="text-sm text-gray-700 mb-1">Select Priority</legend>

  <label class="group block cursor-pointer">
    <input type="radio" name="priority" value="Low" class="peer hidden" checked />
    
    <div class="flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
                group-hover:border-blue-400 peer-checked:border-blue-500 peer-checked:bg-blue-900 transition-all">

      <!-- Outer Radio -->
      <div class="w-5 h-5 flex items-center justify-center rounded-full border-2
                  border-gray-400 peer-checked:border-blue-500 transition-colors">
        <!-- Inner Dot -->
        <div class="w-2.5 h-2.5 rounded-full bg-blue-500 scale-0 peer-checked:scale-100 transition-transform"></div>
      </div>

      <span class="text-sm">Low</span>
    </div>
  </label>

  <label class="group block cursor-pointer">
    <input type="radio" name="priority" value="Medium" class="peer hidden" />
    <div class="flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
                group-hover:border-blue-400 peer-checked:border-blue-500 peer-checked:bg-blue-900 transition-all">
      <div class="w-5 h-5 flex items-center justify-center rounded-full border-2
                  border-gray-400 peer-checked:border-blue-500 transition-colors">
        <div class="w-2.5 h-2.5 rounded-full bg-blue-500 scale-0 peer-checked:scale-100 transition-transform"></div>
      </div>
      <span class="text-sm">Medium</span>
    </div>
  </label>

  <label class="group block cursor-pointer">
    <input type="radio" name="priority" value="High" class="peer hidden" />
    <div class="flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
                group-hover:border-blue-400 peer-checked:border-blue-500 peer-checked:bg-blue-900 transition-all">
      <div class="w-5 h-5 flex items-center justify-center rounded-full border-2
                  border-gray-400 peer-checked:border-blue-500 transition-colors">
        <div class="w-2.5 h-2.5 rounded-full bg-blue-500 scale-0 peer-checked:scale-100 transition-transform"></div>
      </div>
      <span class="text-sm">High</span>
    </div>
  </label>
</fieldset>


Solution

  • TLDR: It's not possible to target nested elements using peer; only with group. You can create a custom variant for group that checks for the presence of input:checked. See custom group-checked variant in the last code snippet for reference.

    Peer

    The peer class only affects direct sibling elements, not their child elements.

    If you absolutely want to solve this using only peer without any group, you can still apply pseudo-elements like before and after on the sibling element. I won't elaborate on this example here - I just wanted to leave the idea itself. I believe my other examples using group are simpler and more reusable.

    Group & group-has-*

    However, you can add a group class to the <label>. With group, you can leverage the CSS :has selector using Tailwind's group-has-* variant, which allows you to target the single input field that has the checked state: group-has-[input:checked]

    <script src="https://cdn.tailwindcss.com"></script>
    
    <fieldset class="space-y-2">
      <!-- Example -->
      <label class="group block cursor-pointer">
        <input type="radio" name="priority" value="Low" class="peer hidden" checked />
        
        <div class="
          flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
          group-hover:border-blue-400
          peer-checked:border-blue-500 peer-checked:bg-blue-900
          transition-all
        ">
    
          <!-- Outer Radio -->
          <div class="
            w-5 h-5 flex items-center justify-center rounded-full border-2 border-gray-400
            group-has-[input:checked]:border-blue-500 transition-colors
          ">
            <!-- Inner Dot -->
            <div class="
              w-2.5 h-2.5 rounded-full bg-blue-500 scale-0
              group-has-[input:checked]:scale-100 transition-transform
            "></div>
          </div>
    
          <span class="text-sm group-has-[input:checked]:text-blue-200">Low</span>
        </div>
      </label>
      
      <!-- Copy for Medium -->
      <label class="group block cursor-pointer">
        <input type="radio" name="priority" value="Low" class="peer hidden" />
        
        <div class="
          flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
          group-hover:border-blue-400
          peer-checked:border-blue-500 peer-checked:bg-blue-900
          transition-all
        ">
    
          <!-- Outer Radio -->
          <div class="
            w-5 h-5 flex items-center justify-center rounded-full border-2 border-gray-400
            group-has-[input:checked]:border-blue-500 transition-colors
          ">
            <!-- Inner Dot -->
            <div class="
              w-2.5 h-2.5 rounded-full bg-blue-500 scale-0
              group-has-[input:checked]:scale-100 transition-transform
            "></div>
          </div>
    
          <span class="text-sm group-has-[input:checked]:text-blue-200">Low</span>
        </div>
      </label>
    </fieldset>

    In this case, I would omit the peer entirely.

    <script src="https://cdn.tailwindcss.com"></script>
    
    <fieldset class="space-y-2">
      <!-- Example -->
      <label class="group block cursor-pointer">
        <input type="radio" name="priority" value="Low" class="hidden" checked />
        
        <div class="
          flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
          group-hover:border-blue-400
          group-has-[input:checked]:border-blue-500 group-has-[input:checked]:bg-blue-900
          transition-all
        ">
    
          <!-- Outer Radio -->
          <div class="
            w-5 h-5 flex items-center justify-center rounded-full border-2 border-gray-400
            group-has-[input:checked]:border-blue-500 transition-colors
          ">
            <!-- Inner Dot -->
            <div class="
              w-2.5 h-2.5 rounded-full bg-blue-500 scale-0
              group-has-[input:checked]:scale-100 transition-transform
            "></div>
          </div>
    
          <span class="text-sm group-has-[input:checked]:text-blue-200">Low</span>
        </div>
      </label>
      
      <!-- Copy for Medium -->
      <label class="group block cursor-pointer">
        <input type="radio" name="priority" value="Low" class="hidden" />
        
        <div class="
          flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
          group-hover:border-blue-400
          group-has-[input:checked]:border-blue-500 group-has-[input:checked]:bg-blue-900
          transition-all
        ">
    
          <!-- Outer Radio -->
          <div class="
            w-5 h-5 flex items-center justify-center rounded-full border-2 border-gray-400
            group-has-[input:checked]:border-blue-500 transition-colors
          ">
            <!-- Inner Dot -->
            <div class="
              w-2.5 h-2.5 rounded-full bg-blue-500 scale-0
              group-has-[input:checked]:scale-100 transition-transform
            "></div>
          </div>
    
          <span class="text-sm group-has-[input:checked]:text-blue-200">Medium</span>
        </div>
      </label>
    </fieldset>

    Custom group-checked variant

    (alternative for group-has-[input:checked])

    And you could even create a custom variant if needed.

    @custom-variant group-checked {
      &:is(:where(.group):has(input:checked) *) {
        @slot;
      }
    }
    

    Now can refer to group-has-[input:checked] as group-checked, see:

    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    <style type="text/tailwindcss">
    @custom-variant group-checked {
      &:is(:where(.group):has(input:checked) *) {
        @slot;
      }
    }
    </style>
    
    <fieldset class="space-y-2">
      <!-- Example -->
      <label class="group block cursor-pointer">
        <input type="radio" name="priority" value="Low" class="hidden" checked />
        
        <div class="
          flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
          group-hover:border-blue-400
          group-checked:border-blue-500 group-checked:bg-blue-900
          transition-all
        ">
    
          <!-- Outer Radio -->
          <div class="
            w-5 h-5 flex items-center justify-center rounded-full border-2 border-gray-400
            group-checked:border-blue-500 transition-colors
          ">
            <!-- Inner Dot -->
            <div class="
              w-2.5 h-2.5 rounded-full bg-blue-500 scale-0
              group-checked:scale-100 transition-transform
            "></div>
          </div>
    
          <span class="text-sm group-checked:text-blue-200">Low</span>
        </div>
      </label>
      
      <!-- Copy for Medium -->
      <label class="group block cursor-pointer">
        <input type="radio" name="priority" value="Low" class="hidden" />
        
        <div class="
          flex items-center gap-3 p-3 rounded border border-gray-500 bg-transparent
          group-hover:border-blue-400
          group-checked:border-blue-500 group-checked:bg-blue-900
          transition-all
        ">
    
          <!-- Outer Radio -->
          <div class="
            w-5 h-5 flex items-center justify-center rounded-full border-2 border-gray-400
            group-checked:border-blue-500 transition-colors
          ">
            <!-- Inner Dot -->
            <div class="
              w-2.5 h-2.5 rounded-full bg-blue-500 scale-0
              group-checked:scale-100 transition-transform
            "></div>
          </div>
    
          <span class="text-sm group-checked:text-blue-200">Medium</span>
        </div>
      </label>
    </fieldset>