2

Assume I have a small component in a web application that represents an expander control, i.e. a header text, an icon to expand / collapse and some content.

A very simple React implementation (pseudo code) could look like this:

const Expander = ({children, title, onExpandToggle, isExpanded}) => (
  <div>
    <div><span>{title}</span><img src={...} onClick={onExpandToggle} /></div>
    {isExpanded && children}
  </div>
);

This implementation shows the icon after the title, so the position of the icon is determined by the length of the title.

It could look something like this:
Example

Now assume that there are multiple like this below each other. It becomes messy:
Example messy

To make this cleaner, all icons should have the same padding from left. The padding should not be fixed but dynamic, so that the longest title determines the position of all icons:
Example clean

Assuming that I want to keep the expander in its own component, is there a CSS way to achieve my goal?

So far, I haven't tried anything as I don't have a starting point. In WPF I would have used something like SharedSizeGroup, but this doesn't exist for CSS.

Bharata
  • 11,779
  • 6
  • 27
  • 42
Daniel Hilgarth
  • 159,901
  • 39
  • 297
  • 411
  • you should look at the `float` property, or at CSS Flexbox – Supersharp Dec 05 '18 at 22:29
  • @Supersharp: This comment seems to not be helpful. How would you actually use those in this scenario? I have worked with flexbox a lot but I don't know how to apply it to this use case. – Daniel Hilgarth Dec 06 '18 at 07:38
  • Set the same width for all of this list elements and make the icon ````float: right```` i think thats what he ment. – Pascal L. Dec 08 '18 at 20:49
  • That doesn't meet the requirements outlined in the question. – Daniel Hilgarth Dec 09 '18 at 08:03
  • Are all of the Expander items held within a container? – wiesion Dec 09 '18 at 08:23
  • just to ensure: you have a list(or table) with such a controls one below another and need them to have the same width only there. in other words you don't need them to have equal width _everywhere_ on the page, right? – skyboyer Dec 09 '18 at 15:30
  • @skyboyer: well, everywhere would be the ultimate goal. For all inside a common container, I think I have received enough answers – Daniel Hilgarth Dec 09 '18 at 15:33
  • yes, I see. but anyway getting all instances to have the same width regardless their location sounds like challenging puzzle :) – skyboyer Dec 09 '18 at 16:29
  • @skyboyer: I absolutely agree. I don't know if it is even possible. – Daniel Hilgarth Dec 09 '18 at 16:33
  • @DanielHilgarth updated my answer. You can achieve it with JS. – Itay Gal Dec 10 '18 at 09:20
  • @ItayGal: Thanks, but if Javascript, then please React and not jQuery. But I was really looking for a CSS only solution. With Javascript, I am able to implement this myself, but I have no idea how to do it with CSS. – Daniel Hilgarth Dec 10 '18 at 10:29
  • JQuery was my shortcut to supply a working example since I'm new to react :), The JS logic is rather simple, so converting it will probably won't take too long. Regarding the possibility to implement it without JS - It's not possible. I added an explanation in my answer - mainly it's because you can't access a parent element with CSS - https://stackoverflow.com/questions/1014861/is-there-a-css-parent-selector – Itay Gal Dec 10 '18 at 11:53

8 Answers8

11

Assuming you'll have a container to your component, you can set display: flex to the inner container and align-self: flex-end to your image.

Then wrap your component/s with a div that has display: inline-block which takes the width of the biggest element inside.

Here's an example:

.container{
  display: inline-block;
  padding: 3px;
}

.item{
  display: flex;
  flex-direction: row; 
  justify-content: space-between;
}

.item .plus{
  width: 15px;
  height: 15px;
  background-image: url("https://cdn1.iconfinder.com/data/icons/mix-color-3/502/Untitled-43-512.png");
  background-size: cover;
  background-repeat: no-repeat;
  align-self: flex-end;
  margin-left: 10px;
}
<div class="container">
  <div class="item">
    <div>Synonyms</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div>Concept</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div>Term</div>
    <div class="plus"></div>
  </div>
</div>

Affecting all the instances without a shared container or fixed width and with CSS alone, is not possible, since there is no way to access a parent element with CSS. Therefore, If you'll have an inner instance (the biggest one) it won't be able to apply it's width to its own parent or any ancestor and it's other children.

If you're after a solution that will set all the instances in the page with the same size without them sharing a container you can achieve it with JS.

Calculate the width of each instance, save the biggest, then set this width for the rest of the instances. In this example I'm also highlighting the biggest item. The items are all around the page and can be inside various divs and displays or without any container.

var biggestWidth = 0;
var biggestItem;

function setWidth() {
  $(".item").each(function() {
    var currentWidth = $(this).width();
    if (currentWidth > biggestWidth) {
      biggestWidth = Math.ceil(currentWidth);
      biggestItem = $(this);
    }
  });

  $(".item").width(biggestWidth);
  biggestItem.addClass("biggest");
}

$(setWidth());
section {
  width: 40%;
  float: left;
  border: 1px solid black;
  border-radius: 3px;
  margin: 10px;
  padding: 10px;
}

.s1 {
  background-color: #e5e5e5;
  display: table;
}

.item {
  display: inline-block;
  clear: both;
  float: left;
}

.txt {
  float: left;
  display: inline-block;
}

.plus {
  width: 15px;
  height: 15px;
  background-image: url("https://cdn1.iconfinder.com/data/icons/mix-color-3/502/Untitled-43-512.png");
  background-size: cover;
  margin-left: 10px;
  float: right;
}

.shift{
  margin-left: 30%;
}

.clear{
  clear: both;
}

.biggest{
  background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<section class="s1">
  <div class="item">
    <div class="txt">Synonyms</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div class="txt">Concept</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div class="txt">Term</div>
    <div class="plus"></div>
  </div>
</section>
<section>
  <div class="item">
    <div class="txt">Synonyms</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div class="txt">Concept</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div class="txt">Term</div>
    <div class="plus"></div>
  </div>
</section>

<section>
  <div class="item">
    <div class="txt">Synonyms - Long</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div class="txt">Concept</div>
    <div class="plus"></div>
  </div>
  <div class="item">
    <div class="txt">Term</div>
    <div class="plus"></div>
  </div>
</section>

<div class="item">
  <div class="txt">Synonyms</div>
  <div class="plus"></div>
</div>
<div class="item">
  <div class="txt">Concept</div>
  <div class="plus"></div>
</div>
<div class="item">
  <div class="txt">Term</div>
  <div class="plus"></div>
</div>

<div class="clear"></div>
<div class="shift">
<div class="item">
  <div class="txt">Synonyms</div>
  <div class="plus"></div>
</div>
<div class="item">
  <div class="txt">Concept</div>
  <div class="plus"></div>
</div>
<div class="item">
  <div class="txt">The bigget item is here</div>
  <div class="plus"></div>
</div>
</div>
Itay Gal
  • 10,075
  • 6
  • 30
  • 68
  • 1
    Alternatively, give the component 100% width and make the icon float right. The parent has to set some width restriction for that to work, though. – Ingo Bürk Dec 09 '18 at 08:34
  • 2
    But then you'll have to set a width to the parent. If the maximum width is unknown your solution won't help much. – Itay Gal Dec 09 '18 at 08:37
  • Yes, I've mentioned that restriction in my comment as well. Just providing a small alternative. – Ingo Bürk Dec 09 '18 at 08:45
1

You can also try using the css display property "table-row" and "table-cell".

style:

   .trow {
        display: table-row;
      }
      .tcell
     {
        display: table-cell;
        padding: 2px;
      }

You can design your component like following.

const Expander = ({children, title, onExpandToggle, isExpanded}) => (
    <div className="trow">
     <div className="tcell">{title}</div>
      <div className="tcell"><img src={...} onClick={onExpandToggle} /></div>
      {isExpanded && children}
    </div>
  );

Example

      .trow {
        display: table-row;
      }
      .tcell
     {
        display: table-cell;
        padding: 2px;
      }
    <div>
      <div class="trow">
        <div class="tcell">Synonyms</div>
        <div class="tcell">X</div>
      </div>
      <div class="trow">
        <div class="tcell">Concept</div>
        <div class="tcell">X</div>
      </div>
      <div class="trow">
        <div class="tcell">Term</div>
        <div class="tcell">X</div>
      </div>
    </div>
PSK
  • 15,515
  • 4
  • 25
  • 38
1

I think that strictly speaking, with all the restrictions you've laid out, the answer is "no, it's not possible".

But since I believe that you need, at any rate, to group those expanders somehow (alternative being that every expander on the page would be horizontally aligned, to which no complete CSS solution exists either), you can simply count on the CSS box model.

<div style="background-color: #efffef; display: inline-block;">
   <div style="position: relative; padding-right: 36px;">
      <span>Title 1</span>
      <span style="position: absolute; right: 0; top: 0;">[x]</span>
   </div>
   <div style="position: relative; padding-right: 36px;">
      <span>Long title 2</span>
      <span style="position: absolute; right: 0; top: 0;">[x]</span>
   </div>
   <div style="position: relative; padding-right: 36px;">
      <span>Title 3, at your service</span>
      <span style="position: absolute; right: 0; top: 0;">[x]</span>
   </div>
</div>
Sami Hult
  • 2,869
  • 1
  • 9
  • 15
1

I personally would do it with classic <table>. Its easy, semantically correct, and it will be done in 3 minutes. But we could do it also with display:inline-block and float:right, or with <table>-imitation using display:table, display:table-row and display:table-cell. In my opinion display: flex is relative new and not overall cross-browser compatible (for example IE10, IE11 and others). Because of all it I would like to provide you 3 solutions.

Solution with display:inline-block and float:right

With display:inline-block the size is always aligned on the content.

.container,
.item div,
.item b{display:inline-block}
.item b
{
    float:right;
    width:16px;
    height:16px;
    margin-left:5px;
    cursor:pointer;
    background:url("https://i.stack.imgur.com/bKWrw.png")
}
<div class="container">
    <div class="item">
        <div>Synonyms</div>
        <b></b>
    </div>
    <div class="item">
        <div>Concept</div>
        <b></b>
    </div>
    <div class="item">
        <div>Term</div>
        <b></b>
    </div>
</div>
<br style="clear:both">

Solution with <table>-imitation

.container{display:table}
.item{display:table-row}
.item div{display:table-cell; vertical-align:top}
.item b
{
    display:inline-block;
    width:16px;
    height:16px;
    cursor:pointer;
    margin-left:5px;
    background:url("https://i.stack.imgur.com/bKWrw.png")
}
<div class="container">
    <div class="item">
        <div>Synonyms</div>
        <div><b></b></div>
    </div>
    <div class="item">
        <div>Concept</div>
        <div><b></b></div>
    </div>
    <div class="item">
        <div>Term</div>
        <div><b></b></div>
    </div>
</div>

Solution with classic <table> (my favorite)

.menu td{vertical-align:top}
.menu td b
{
    display:inline-block;
    width:16px;
    height:16px;
    cursor:pointer;
    margin-left:5px;
    background:url("https://i.stack.imgur.com/bKWrw.png")
}
<table class="menu" cellpadding="0" cellspacing="0">
<tr>
    <td>Synonyms</td>
    <td><b></b></td>
</tr>
<tr>
    <td>Concept</td>
    <td><b></b></td>
</tr>
<tr>
    <td>Term</td>
    <td><b></b></td>
</tr>
</table>
Bharata
  • 11,779
  • 6
  • 27
  • 42
  • 2
    Yes, table is a great solution for this problem, but then your component is not generic, you'll have to surround it with a table, not the best practice in my opnion. Flex is not new, and it's supported in all the browsers except IE9 and older https://caniuse.com/#search=flexbox – Itay Gal Dec 09 '18 at 14:01
  • `display: grid` can replace old tables with greater flexibility – skyboyer Dec 09 '18 at 15:28
  • "semantically correct" - it really isn't! `table`s are for tabular data. – vzwick Dec 12 '18 at 11:26
1

The simplest example I can think of, based on your code snippet, would be something like:

 const containerStyle = {
   width: `[whatever width you need for the container]`,
   display: `flex`,
   flexDirection: `column`    
 }

 const itemStyle = { 
  width: `100%`, 
  display: `flex`, 
  flexDirection: `row`, 
  justifyContent: `space-between` 
 }

const Expander = ({children, title, onExpandToggle, isExpanded}) => (
  <div style={containerStyle}>
    <div style={itemStyle}>
      <span>{title}</span>
      <img src={...} onClick={onExpandToggle} />
    </div>
    {isExpanded && children}
  </div>
);
vm909
  • 481
  • 3
  • 7
1
const Expander = (children, title, onExpandToggle, isExpanded) => (
   <div>
     {title}
     <img src={...} style={{marginLeft : '5px',float : 'right'}} onClick={onExpandToggle}/>
   </div>
   <div style={{width : '0px',overflowX : 'visible'}}>
     {isExpanded && <div>
       {children}
     </div>}
   </div>
);

And put it in:

<div style={{display : 'inline-block'}}>
   <Expander />
   <Expander />
</div>
Hammad Akhwand
  • 700
  • 1
  • 8
  • 16
1

In CSS it is not possible for different elements of a page to "know" each other. The closest thing you can get is to use display flex and/or grid. This way the direct parent can control the way its children will display, kind of similar to React.

The main difference between flex and grid is that flex is 1 axis and grid 2 axis.

Working example using grid and flex:

<div class="container">
    <div class="summary"><span class="text">Synonyms</span> <span class="icon">+</span></div>
    <div class="details">
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet officiis sequi perspiciatis sapiente iure minus laudantium! Quidem iure consectetur quod odit iusto, labore sunt quae, enim nesciunt officiis, quia ad.</p>
    </div>
    <div class="summary"><span class="text">Concept</span> <span class="icon">+</span></div>
    <div class="details">
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet officiis sequi perspiciatis sapiente iure minus laudantium! Quidem iure consectetur quod odit iusto, labore sunt quae, enim nesciunt officiis, quia ad.</p>
    </div>
    <div class="summary"><span class="text">Terms</span> <span class="icon">+</span></div>
    <div class="details">
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet officiis sequi perspiciatis sapiente iure minus laudantium! Quidem iure consectetur quod odit iusto, labore sunt quae, enim nesciunt officiis, quia ad.</p>
    </div>
</div>
<style>
    .container {
        display: grid;
        /* summary's width needs to be relative to the longest text
           and details needs to span the whole grid */
        grid-template-columns: auto auto; 
    }
    .summary {
        grid-column: 1 / 2; /* start at the beginning of 1st column and end at the beginning of the 2nd column */
        display: flex; /* create horizontal layout */
        justify-content: space-between; /* place the available space between the children */
    }
    .details {
        grid-column: 1 / 3; /* start at the beginning of 1st column and end at the end of the 2nd column */
    }
</style>

As you can see, it is very readable and requires very little markup.

With flex and grid you need to think on 2 levels parent > child. This means you need to change your React component accordingly:

const Expander = ({children, title, onExpandToggle, isExpanded}) => (
    <>
        <div class="summary" onClick={onExpandToggle}><span class="title">{title}</span> <span class="icon">+</span></div>
        {isExpanded && (
            <div class="details">
                {children}
            </div>
        )}
    </>
);

Here I removed the wrapper div in order to use a Fragment to ensure .summary and .details are direct children of a .container. Note that I also moved the onClick on the whole title for UX reasons.

CSS Grid is very useful and flexible, it is worth learning it. The browser support is recent but quite good, just make sure you use autoprefixer.

You can also use tricks using display: inline-block; whose size is based on its children but will involve workarounds when one of the children needs to overflow (like in your case). So I don't recommend it. I would also advise against using Javascript.

Don't hesitate to tell me what you think about this answer.

Andréa Maugars
  • 405
  • 3
  • 7
1

It is possible.

1. Flex-box + Absolution:

if you don't mind to use absolute positioned container, you can do it in this way

.container {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  width: auto;
}

.item {
  margin: 5px 0;
  padding: 5px;
  border: 1px solid black;
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.header img {
  width: 8px;
  height: 8px;
  margin-left: 20px;
  cursor: pointer;
}
<div class="container">
  <div class="item">
    <div class="header">
      <span>Synonyms</span>
      <img src="https://image.flaticon.com/icons/png/512/61/61155.png" />
    </div>
  </div>
  <div class="item">
    <div class="header">
      <span>Concept</span>
      <img src="https://image.flaticon.com/icons/png/512/61/61155.png" />
    </div>
  </div>
  <div class="item">
    <div class="header">
      <span>Term</span>
      <img src="https://image.flaticon.com/icons/png/512/61/61155.png" />
    </div>
  </div>
</div>

The main problem here is that you don't know the height of the container so it might influence other content.

2. Inline Flex-box:

if the container position is critical for you, you can use inline-flex

.wrapper {
  margin: 20px 10px;
}

.container {
  display: inline-flex;
  flex-direction: column;
  align-items: stretch;
  width: auto;
}

.item {
  margin: 5px 0;
  padding: 5px;
  border: 1px solid black;
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.header img {
  width: 8px;
  height: 8px;
  margin-left: 20px;
  cursor: pointer;
}
Some content above the container...
<div class="wrapper">
  <div class="container">
    <div class="item">
      <div class="header">
        <span>Synonyms</span>
        <img src="https://image.flaticon.com/icons/png/512/61/61155.png" />
      </div>
    </div>
    <div class="item">
      <div class="header">
        <span>Concept</span>
        <img src="https://image.flaticon.com/icons/png/512/61/61155.png" />
      </div>
    </div>
    <div class="item">
      <div class="header">
        <span>Term</span>
        <img src="https://image.flaticon.com/icons/png/512/61/61155.png" />
      </div>
    </div>
  </div>
</div>
Some content below the container...
Roman Maksimov
  • 1,281
  • 1
  • 7
  • 13