htmlcssz-indexstacking-context

Table row shadow below all data cells


Im having trouble making a valid HTML table with vertical spacing between rows and shadow below each row.

The shadow always goes on top of other table data.

I have positioned the elements and set a z-index.

enter image description here

  table {
    border-collapse: separate;
    border-spacing: 0;
  }
  td,
  th {
    min-width: 170px;
  }

  .shadow {
    position: relative;
    z-index: 1;
    margin: 2px 0 2px 0;
  }

  .shadow:before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: -1;
    box-shadow: 0 0 10px 10px #000;
  }
<main>
  <table>
    <thead>
      <tr>
        <td>head</td>
        <td>head</td>
        <td>head</td>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><div class="shadow">div</div></td>
        <td><div class="shadow">div</div></td>
        <td><div class="shadow">div</div></td>
      </tr>
      <tr>
        <td><div class="shadow">div</div></td>
        <td><div class="shadow">div</div></td>
        <td><div class="shadow">div</div></td>
      </tr>
      <tr>
        <td><div class="shadow">div</div></td>
        <td><div class="shadow">div</div></td>
        <td><div class="shadow">div</div></td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <td>foot</td>
        <td>foot</td>
        <td>foot</td>
      </tr>
    </tfoot>
  </table>
</main>


Solution

  • One approach is as below, with explanatory comments in the code itself:

    /* removing all default padding and margins, and ensuring
       that all elements are sized to include their padding
       and border-widths: */
    *,
    ::before,
    ::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    table {
      /* CSS custom properties for various properties to ensure
         common styling where appropriate;
          --tr-space-between is the size of the percieved/visible
         gap between adjacent rows: */
      --tr-space-between: 0.5rem;
      /* --td-padding-block is derived from the previous variable,
         using calc() so that the space between 'rows' is inserted
         as padding (padding-block) to create that row-spacing: */
      --td-padding-block: calc(var(--tr-space-between)/2);
      --shadow-color: lightgray;
      --row-color: #fff;
      /* the desired radius of the 'rows': */
      --row-radius: 0.5rem;
      /* to ensure page background can be seen (if required)
         in the row-gaps: */
      background-color: transparent;
      /* collapsing the borders between cells in order to allow the
         content to be contiguous, and using a different means to
         achieve row-"separation": */
      border-collapse: collapse;
      border-spacing: 0;
      /* using a CSS filter, drop-shadow(), to create the shadows: */
      filter: drop-shadow(0 0 0.5rem var(--shadow-color));
      /* centering the <table> */
      margin-inline: auto;
    }
    
    td,
    th {
      min-width: 170px;
    }
    
    td,
    th {
      /* again to ensure that the page background is - where
         appropriate - visible through the visual gaps: */
      background-color: transparent;
    }
    
    td {
      /* setting the cell padding on the block axis, to "separate"
         the "rows", while no padding is applied on the inline
         axis, so that the rows are visually contiguous: */
      padding-block: var(--td-padding-block);
    }
    
    /* using logical properties to set the border radii: */
    td:first-child .content {
      border-start-start-radius: var(--row-radius);
      border-end-start-radius: var(--row-radius);
    }
    
    td:last-child .content {
      border-start-end-radius: var(--row-radius);
      border-end-end-radius: var(--row-radius);
    }
    
    .content {
      /* setting the background-color of the "row": */
      background-color: var(--row-color);
      /* applying padding on all axes, to move the content
         away from the edges of the 'row': */
      padding: 0.5rem;
    }
    <main>
      <table>
        <thead>
          <tr>
            <th>head</th>
            <th>head</th>
            <th>head</th>
          </tr>
        </thead>
        <tbody>
          <!-- I've changed the class-name of the <div> from 'shadow' to
               'content' to reflect what the "purpose" of the element: -->
          <tr>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
          </tr>
          <tr>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
          </tr>
          <tr>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
          </tr>
        </tbody>
        <tfoot>
          <tr>
            <td>foot</td>
            <td>foot</td>
            <td>foot</td>
          </tr>
        </tfoot>
      </table>
    </main>

    There are, of course, other approaches; I've shown a couple more in the following example, again: explanatory comments are in the code:

    const changeBackground = (evt) => {
      let {
        currentTarget
      } = evt, {
        value
      } = currentTarget,
      table = document.querySelector('table');
    
      table.dataset.shadow = value;
    }
    
    document.querySelectorAll('input[type=radio]').forEach(
      (el) => el.addEventListener('change', changeBackground)
    );
    
    document.querySelector('input:checked').dispatchEvent(new Event('change'));
    /* removing all default padding and margins, and ensuring
       that all elements are sized to include their padding
       and border-widths: */
    
    *,
    ::before,
    ::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      /* a relatively simple background to show how the transparencies
         might be beneficial; otherwise irrelevant to the demo: */
      background-image:
        repeating-linear-gradient(
          to bottom left,
          transparent,
          hsl(140deg 90% 80% / 0.7)
        ), radial-gradient(
          at 0 0,
          hsl(300deg 95% 85% / 1),
          hsl(180deg 95% 85% / 1)
        );
      block-size: 100vh;
      padding: 1rem;
    }
    
    form {
      inline-size: 70%;
      margin-block: 1rem;
      margin-inline: auto;
    }
    
    fieldset {
      border: 0 none transparent;
      display: flex;
      flex-flow: row wrap;
      gap: 1rem;
      justify-content: space-between;
      padding: 1rem;
    }
    
    legend {
      font-size: 1.2rem;
      position: relative;
    }
    
    label {
      flex-grow: 1;
    }
    
    .labelText {
      /* using a CSS custom property to simplify the CSS: */
      --active-indication: hsl(0deg 75% 70% / 0.55);
      background-color: #cccd;
      /* using a background-image to give show whether a
         selection has, or has not, been made; this is the
         default 'inactive' state: */
      background-image:
        linear-gradient(
        270deg,
        var(--active-indication) 0 1.5rem,
        /* gives the illusion of a border separating the
           background-color of the element, and the
           'activation'/checked state: */
        #0009 1.5rem calc(1.5rem + 1px),
        /* transparent, to allow the background to show through: */
        transparent calc(1.5rem + 1px)
      );
      border: 1px solid #0009;
      border-radius: 1rem;
      display: block;
      padding: 0.75rem;
      padding-inline-end: 2rem;
    }
    
    /* moving the radio inputs off screen to hide them: */
    input[type=radio] {
      position: absolute;
      left: -2000px;
    }
    
    /* this selector matches the .labelText that immediately
       follows a checked <input>; despite being hidden off-screen
       the <input> still precedes the .labelText element in
       the DOM: */
    input:checked + .labelText {
      /* here we update the custom property, which updates
         the background-image/linear-gradient: */
      --active-indication: hsl(160deg 95% 75% / 1);
    }
    
    table {
      /* CSS custom properties for various properties to ensure
         common styling where appropriate;
          --tr-space-between is the size of the percieved/visible
         gap between adjacent rows: */
      --tr-space-between: 0.5rem;
      /* --td-padding-block is derived from the previous variable,
         using calc() so that the space between 'rows' is inserted
         as padding (padding-block) to create that row-spacing: */
      --td-padding-block: calc(var(--tr-space-between)/2);
      --shadow-color: #339a;
      --row-color: #fff;
      --row-inset: 1rem;
      /* the desired radius of the 'rows': */
      --row-radius: 0.5rem;
      /* to ensure page background can be seen (if required)
         in the row-gaps: */
      background-color: transparent;
      /* collapsing the borders between cells in order to allow the
         content to be contiguous, and using a different means to
         achieve row-"separation": */
      border-collapse: collapse;
      border-spacing: 0;
      margin-block: 1rem;
      /* centering the <table> */
      margin-inline: auto;
    }
    
    /* using attribute-selectors along wtih custom data-* attributes
       to appropriately style the <table> based on the choice made as
       to the approach: */
    table[data-shadow="drop-shadow"] {
      /* using a CSS filter, drop-shadow(), to create the shadows: */
      filter: drop-shadow(0 0 2rem var(--shadow-color));
    }
    
    table[data-shadow="box-shadow"] {
      /* using a box-shadow, note that this provides a shadow *around*
         the element, but not between the 'rows' (this is why I didn't
         use box-shadow in the original code, and this is simply to
         illustrate that point): */
      box-shadow: 0.5rem 0.5rem 2rem var(--shadow-color);
    }
    
    table[data-shadow="backdrop-filter"] {
      /* this allows a number of different functions to be used to
         to style the view of whatever is visible "through" the
         background of the <table> element: */
      backdrop-filter: hue-rotate(245deg) opacity(0.4);
    }
    
    td,
    th {
      min-width: 170px;
    }
    
    td,
    th {
      /* again to ensure that the page background is - where
         appropriate - visible through the visual gaps: */
      background-color: transparent;
    }
    
    td {
      /* setting the cell padding on the block axis, to "separate"
         the "rows", while no padding is applied on the inline
         axis, so that the rows are visually contiguous: */
      padding-block: var(--td-padding-block);
    }
    
    
    /* using logical properties to set the border radii: */
    
    td:first-child .content {
      border-start-start-radius: var(--row-radius);
      border-end-start-radius: var(--row-radius);
    }
    
    td:last-child .content {
      border-start-end-radius: var(--row-radius);
      border-end-end-radius: var(--row-radius);
    }
    
    td:first-child {
      padding-inline-start: var(--row-inset);
    }
    
    td:last-child {
      padding-inline-end: var(--row-inset);
    }
    
    .content {
      /* setting the background-color of the "row": */
      background-color: var(--row-color);
      /* applying padding on all axes, to move the content
         away from the edges of the 'row': */
      padding: 0.5rem;
    }
    <main>
      <form action="#" id="choices" method="post">
        <fieldset>
          <legend>Choose approach</legend>
          <label>
            <input type="radio" name="shadowType" value="drop-shadow" checked>
            <span class="labelText"><code>filter: drop-shadow()</code></span>
          </label>
          <label>
            <input type="radio" name="shadowType" value="box-shadow" >
            <span class="labelText"><code>box-shadow</code></span>
          </label>
          <label>
            <input type="radio" name="shadowType" value="backdrop-filter" >
            <span class="labelText"><code>backdrop-filter</code></span>
          </label>
        </fieldset>
      </form>
      <table>
        <thead>
          <tr>
            <th>head</th>
            <th>head</th>
            <th>head</th>
          </tr>
        </thead>
        <tbody>
          <!-- I've changed the class-name of the <div> from 'shadow' to
               'content' to reflect what the "purpose" of the element: -->
          <tr>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
          </tr>
          <tr>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
          </tr>
          <tr>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
            <td>
              <div class="content">div</div>
            </td>
          </tr>
        </tbody>
        <tfoot>
          <tr>
            <td>foot</td>
            <td>foot</td>
            <td>foot</td>
          </tr>
        </tfoot>
      </table>
    </main>