Search code examples
htmlcssflexboxcss-grid

Is it possible to adjust div spacing with just css when user data is dynamically absent?


What I'm trying to achieve: I have a card element that has an image on the left, with a number of rows on the right. With grid, it might look something like this:

.container>* {
  border: 1px solid black;
}

.container {
  display: grid;
  grid-template-columns: [first] 70px [second] 1fr;
  grid-template-rows: 1fr 25% 25% 25% [lastRow];
  height: 70px;
  width: 220px;
  grid-template-areas: "date title" "date location" "date notes" "date links";
}

.title {
  grid-area: title;
  overflow: hidden;
}

.location {
  grid-area: location;
}

.notes {
  grid-area: notes;
}

.links {
  grid-area: links;
}

.date {
  grid-area: date;
}
<div class='container'>
  <div class='title'>Title that is super long and we are going to have a problem fitting it</div>
  <div class='location'>location</div>
  <div class='notes'>notes</div>
  <div class='links'>links</div>
  <div class='date'>date</div>
</div>

There are a few conditions: some of the the rows might not be present (only title is guaranteed), and title might be super long. Overflow is okay, but if rows are missing below title, I would like it to take up as much of that space as possible, while still allowing overflow to work. Ideally, all of the other rows below would go to the bottom (to give title as much space as possible), but I'm able to manage that on the server side by changing which areas the lines go to (but I would rather not change the css because there are multiple of these cards on the page). Also, if all rows are present, they should take up the same amount of height.

Question: Is this possible with just css?

What I've tried: I tried adding grid-row-end:lastRow; to the .title class, but while this helped me achieve the spacing effect, it broke overflow, as the element now thought that it had all of the space available to it up until the end of the card, so text appeared positioned behind other elements.

I also tried doing this through flex. This was actually the closest I got, but I wasn't able to figure out how to get the flex-grow parameter to adjust the title row to take up more space when an element was missing (see my attempt below, and note what happens if you remove everything inside one of the divs with item2 or item3). I set the flex-grow to be 1 for all elements, since they need to be the same when they're all present, but set it to 0 for empty elements. This deletes them, but spreads the height between remaining items equally, but I want all of that extra height to go to the title element. I also couldn't figure out if I could achieve this with flex-basis.

Edit: I ended up going with the flexbox solution, because it has a very big advantage over the grid solution (at least in my testing): all lines can become bigger if they need to be, right out of the box.

I did make a few changes to the accepted answer:

One issue was when the non title lines were also long, they grew preferentially. To get around this, I added min-height:<font-height> in container>* and add min-height:0px to .container>:empty. Then I gave the titlerow flex-grow: 2, and flex: 1 1 auto for all other elements. This allows to gracefully handle multiple rows being too long.

Since I also wanted to maintain equal height when the elements all present, I set max-height:25% in .container >*, and unset this in the :has selector, to make sure that empty elements stay hidden. But if the equal line requirement isn't necessary, you can ditch the max-height and the whole :has selector.

p {
  margin: 0;
  padding-left: 1%;
  padding-right: 1%;
  text-align: center;
  max-height: 100%;
}

.title {
  overflow: hidden;
}

.container {
  display: flex;
  border: 1px solid red;
  flex-direction: column;
  height: 150px;
  width: 80px;
}

.container>* {
  flex: 1 1;
  border: 1px solid #000;
  display: flex;
  justify-content: center;
  align-items: center;
}

.container> :first-child {
  flex: 1 1;
}

.container> :empty {
  flex-grow: 0;
  border: 0px;
  padding: 0px;
}
<div class="container">
  <div class="title">
    <p>item1 a really long piece of text that doesn't fit into our container at all</p>
  </div>
  <div>
    <p>item2</p>
  </div>
  <div>
    <p>item3</p>
  </div>
</div>


Solution

  • Declaring a value for flex-basis in the flex shorthand is optional. When omitted, flex-basis is given 0 (replacing the default value "auto".)

    Going with the Flexbox example, you can use the :has() pseudo-class to declare flex-basis: auto on the child elements when there are empty child elements.

    Can I use :has()? currently shows 87.03% support. (It's not enabled by default in Firefox, but that's less than 3% of users.)

    I also want to point out that the :empty element must be completely empty with no whitespace, linebreaks, etc.

    .container:has(> :empty) > * {
      flex-basis: auto;
    }
    

    p {
      margin: 0;
      padding: 0 1%;
      text-align: center;
      max-height: 100%;
    }
    
    .title {
      flex-grow: 1;
      overflow: hidden;
    }
    
    .container {
      display: flex;
      border: 1px solid red;
      flex-direction: column;
      height: 150px;
      width: 80px;
    }
    
    .container > * {
      display: flex;
      flex: 1 1;
      justify-content: center;
      align-items: center;
      border: 1px solid;
    }
    
    .container > :empty {
      flex-grow: 0;
      border: 0;
    }
    
    .container:has(> :empty) > * {
      flex-basis: auto;
    }
    <div class="container">
      <div class="title">
        <p>row1 a really long piece of text that doesn't fit into our container at all</p>
      </div>
      <div>
        <p>row2</p>
      </div>
      <div>
        <p>row3</p>
      </div>
      <div>
        <p>row4</p>
      </div>
    </div>
    
    <hr>
    
    <div class="container">
      <div class="title">
        <p>row1 a really long piece of text that doesn't fit into our container at all</p>
      </div>
      <div>
        <p>row2</p>
      </div>
      <div></div>
      <div>
        <p>row4</p>
      </div>
    </div>
    
    <hr>
    
    <div class="container">
      <div class="title">
        <p>row1 a really long piece of text that doesn't fit into our container at all</p>
      </div>
      <div></div>
      <div>
        <p>row3</p>
      </div>
      <div></div>
    </div>