Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Margin (vertical space) of the first block after a text node?

Tags:

First question:

Inside a block such as <dd> or <td> or <li>, am I right in saying that either or both of the following would be normal and correct?

<td>
  An introductory sentence as a text node.
  <p>A further sentence as a paragraph.</p>
</td>

And/or:

<td>
  <p>An introductory sentence as a paragraph.</p>
  <p>A further sentence as a paragraph.</p>
</td>

Second question:

Assuming that's so, what CSS can I use to ensure that in both cases there's a margin between the two sentences, but not before the first sentence?

My problem is that if I do something like ...

td > p { margin-top: 1em; }

... this works correctly when the first sentence is a text node, but it puts an unwanted margin between the first sentence and the containing <td> when the first sentence is inside a paragraph.

I would want a solution that works even when the first sentence contains an inline element, such as <strong> or <a href="...">.


In summary CSS should produce the same layout, as follows, for both of the two HTML examples:

-- top of <td> here
-- no margin here
-- first sentence here (whether it's a text node and/or inside a <p>)
-- margin here, between the two sentences
-- second sentence here inside a <p>

Third question:

If it's true that it's not possible to define a CSS rule which creates the same format for both kinds of HTML, then I guess that web site authors must be careful to always use only one HTML markup?

If so, which you do suppose it normal?

  • Never start a block with another block, always start it with a text node (then you could use the CSS rule which says that the first block should have a margin-top); for example, do this:

    <td>
      An introductory sentence as a text node.
      <p>A further sentence as a paragraph.</p>
    </td>
    

    This wouldn't work though if for example the first thing in the block is meant to be a list -- the list would have unwanted margin-top:

    <td>
      <ul>
        <li>List item</li>
        <li>List item</li>
      </ul>
    </td>
    
  • Never start a block with a text node, always start it with a block (then you could use the CSS rule which says that the first block shouldn't have a margin-top); for example, do this:

    <td>
      <p>An introductory sentence as a paragraph.</p>
      <p>A further sentence as a paragraph.</p>
    </td>
    

    That's more markup than I expected though. I think I'm used to seeing markup like <li>List item</li> rather than <li><p>List item</p></li>

  • Don't standardize, use either, but add extra class= or style= attributes to blocks as necessary, to add or remove a top margin on a case-case basis.

like image 788
ChrisW Avatar asked Jul 26 '17 21:07

ChrisW


People also ask

What are block margins?

margin-block is a shorthand property in CSS that sets an element's margin-block-start and margin-block-end values, both of which are logical properties. It creates space around the element in the block direction, which is determined by the element's writing-mode , direction , and text-orientation .

What does margin-block start mean?

The margin-block-start CSS property defines the logical block start margin of an element, which maps to a physical margin depending on the element's writing mode, directionality, and text orientation.

What is margin-block HTML?

The margin-block CSS shorthand property defines the logical block start and end margins of an element, which maps to physical margins depending on the element's writing mode, directionality, and text orientation.

What is padding vs margin?

In CSS, a margin is the space around an element's border, while padding is the space between an element's border and the element's content. Put another way, the margin property controls the space outside an element, and the padding property controls the space inside an element.


2 Answers

I'm not sure if you would consider this to be a complete answer to your question, as I'm aware you are using a <td> in your examples and one difference between the <td> and <dd> or <li> elements is the fact that <td> elements can't be offset against their surrounding elements without breaking their <table> specific behaviour. But at the very least I can answer a part of your third question:

If it's true that it's not possible to define a CSS rule which creates the same format for both kinds of HTML...

That isn't true, you can always render a floating :before pseudo element with a width of 100%; set, and setting half the margin of the sibling <p> elements on it.

dd {
  border: 1px dashed lightblue; /* this line is for demonstration purposes only */
}

dd:before {
  float: left;
  content: "";
  width: 100%;
  margin: 0.5em;
}
<dd>
  An introductory sentence as a text node.
  <p>A further sentence as a paragraph.</p>
</dd>
<dd>
  <p>An introductory sentence as a paragraph.</p>
  <p>A further sentence as a paragraph.</p>
</dd>

This introduces an empty "dummy" paragraph, which only affects direct text nodes of the <dd> as the <p> elements will just perform their automatic margin collapsing magic instead of moving downwards. I think this proves that it is at least possible to define a CSS rule which creates the same format for both kinds of HTML.

Here's how this works, or at least how I understand this to work. The W3C has an example in the CSS spec which shows us that the text node in the question must be an anonymous block box, because it is a text node rendered inside a box with display: block; set, the <dd>, and it has a sibling with display: block; set, the <p>.

By adding the pseudo element - which is rendered inside this anonymous block box (it must be, because otherwise it would never be able to behave like an inline element, or else it may be rendered as two anonymous inline boxes, without the containing block box) - but in any case, we end up with two anonymous inline boxes (the pseudo element and the text node).

The next step is to get the first of these anonymous inline boxes, the pseudo element, and take it out of the normal flow by floating it left, then setting its width to 100%, and making it take up a height which matches the height of the sibling <p>'s margin (which I accomplished by setting a margin of half the <p>'s margin, but you could do the same by setting a height or bottom margin matching the <p>'s margin).

Now the preceding text node has an artificial top margin. The question remains, why does this not affect the <p> elements if there is no preceding text node? I think that's because - since there is no preceding text node - the pseudo element is itself rendered as an empty anonymous block box inside the element on which it is applied (as pseudo elements with content always render inside the element on which they are applied), which is essentially the same as rendering an empty <span> element before the <p>.

Here's a proof of concept:

dd {
  border: 1px dashed lightblue;
}

span {
  float: left;
  height: 1em;
  width: 100%;
  background-color: lightgray;
}

dd:not(:first-child)::before {
  content: "";
  float: left;
  height: 1em;
  width: 100%;
  background-color: lightgray;
}
<dd>
  <span></span>
  <p>
    The dashed light blue line marks the paragraphs margin box, the light grey box is the span.
  </p>
</dd>
<dd>
  <p>
    The dashed light blue line marks the paragraphs margin box, the light grey box is the pseudo element.
  </p>
</dd>

This "artificial margin" is a left floated block box (anonymous block box in case of the pseudo element) inside its containing element. All other block boxes will move down if they need to (as they are supposed according to the W3C floating spec when the floated box doesn't leave any space for them), this only happens as the floating box starts to outgrow the margin in which it is hiding, and it doesn't happen in the solution to this specific problem, as I have specifically set the artificial margin to be exactly as high as the actual margin of the <p>'s.

I think the secret lies in this part of the W3C floating spec, which is a little hard to follow:

Since a float is not in the flow, non-positioned block boxes created before and after the float box flow vertically as if the float did not exist. However, the current and subsequent line boxes created next to the float are shortened as necessary to make room for the margin box of the float.

A line box is next to a float when there exists a vertical position that satisfies all of these four conditions: (a) at or below the top of the line box, (b) at or above the bottom of the line box, (c) below the top margin edge of the float, and (d) above the bottom margin edge of the float.

Note: this means that floats with zero outer height or negative outer height do not shorten line boxes.

If a shortened line box is too small to contain any content, then the line box is shifted downward (and its width recomputed) until either some content fits or there are no more floats present. Any content in the current line before a floated box is reflowed in the same line on the other side of the float. In other words, if inline-level boxes are placed on the line before a left float is encountered that fits in the remaining line box space, the left float is placed on that line, aligned with the top of the line box, and then the inline-level boxes already on the line are moved accordingly to the right of the float (the right being the other side of the left float) and vice versa for rtl and right floats.

I would understand this to mean "non-positioned block boxes created before and after the float box flow vertically as if the float did not exist", so the <p>s, which are non positioned block boxes should not be affected by the floating box.

But that's not what it means. Instead it states that, when the box is floated left, a line box is created to the right of the floating box, filling the space between the right side of the floating box and the right side of the containing box. And inside that line box lives the block box which is the succeeding <p> element. And if that <p> element could fit in the space which satisfies the four conditions mentioned, it would sit next to the float in the line box.

Since the float is set to a width of 100%, the <p> doesn't fit next to the floated box, and it, sitting inside its line box, moves down to the next line, where it somehow magically decides to partly honour the first part of the rule: "non-positioned block boxes created before and after the float box flow vertically as if the float did not exist", which only seems to be true for the margin, because as soon as the float box outgrows the margin, the block box does start to move down, maybe because its sitting in a line box as well..

Now for anything except a <td> tag it would be quite trivial to have the added space on top disappear as it can easily be done by offsetting the element containing the content against its containing element.

dd {
  position: absolute;
  margin-top: -1em;      
}

dd:before {
  float: left;
  content: "";
  width: 100%;
  margin: 0.5em;
}

div {
  position: relative;
  /* everything below this line is for demonstration purposes only */
  border-top: 1px dashed lightblue;
  height: 80px;
}
<div>
  <dd>
    An introductory sentence as a text node.
    <p>A further sentence as a paragraph.</p>
  </dd>
</div>
<div>
  <dd>
    <p>An introductory sentence as a paragraph.</p>
    <p>A further sentence as a paragraph.</p>
  </dd>
</div>

Which I think answers the second question, at least for <dd> and <li> elements, even allowing for inline elements in the preceding text node.

If you wanted to do that inside a <td> you'd have to start managing the <td> or <table> height some other way, as you'll have to use absolute positioning inside the <td> and break the default table behaviour where the table grows with its content by setting the table cell to display: block; (or by rendering an additional <div> inside the <td> and using that as the block level element, but that would also break the default cell growing behaviour).

table
{
  width: 100%;
  min-height: 80px;
  float: left;
}

dd {
  position: absolute;
  margin-top: -1em;
}

dd:before {
  float: left;
  content: "";
  width: 100%;
  margin: 0.5em;
}

td {
  position: relative;
  display: block;
  border-top: 1px dashed lightblue; /* this line is for demonstration purposes only */  
}
<table>
  <tr>
    <td>
      <dd>
        An introductory sentence as a text node.
        <p>A further sentence as a paragraph.</p>
      </dd>
    </td>
  </tr>
</table>
<table>
  <tr>
    <td>
      <dd>
        <p>An introductory sentence as a paragraph.</p>
        <p>A further sentence as a paragraph.</p>
      </dd>
    </td>
  </tr>
</table>
like image 143
tom-19 Avatar answered Sep 30 '22 21:09

tom-19


First Answer:

It's permitted per current HTML spec (HTML5). However, the Strict flavors of previous generation of HTML (HTML4/XHTML1) required certain elements (e.g. body or form) to contain only blocks, not bare text or text level elements (now collectively referred as "phrasing content"). The newer spec had to relax this limitation in order to match reality, but I believe it had a rationale, primarily because of the next point...

Second Answer:

As Mathijs Flietstra said in his answer, any run of text placed in the same container as block level elements without any display:block wrapper ends up being the anonymous block box (because a CSS block can contain either inline boxes or block boxes, but not both kinds of them). These anonymous boxes are behave like usual block boxes (think div elements), but you can't set them any CSS property directly, they only get the inheritable properties from the container. So there is no way to set them neither padding nor margin nor border nor background. All existing CSS selectors just don't take them into account (except :empty pseudo class, but it isn't very helpful in this case).

Yes, sometimes you can find a hack to work around this limitation. Moreover, modern CSS mechanisms like Grid Layout provide ways, e.g., to have uniform spacing between chilren of the container regardless their type:

div > * {
  margin:0;
}
div{
  display:grid;
  grid-columns: 1fr;
  grid-row-gap: 1em;
  border:1px solid black;
}
<div>
  <p>An introductory sentence as a paragraph.</p>
  <p>A further sentence as a paragraph.</p>
  <p>Another further sentence as a paragraph.</p>
</div>

<div>
  An introductory sentence as a text node.
  <p> A further sentence as a paragraph.</p>
  <address>Another further sentence as an <code>address</code> element.</address>
</div>

However, neither of these workariunds is a silver bullet. For example, you can't apply display:grid to a table cell without breaking the table structure.

Third answer:

Given that CSS block can contain only one type of boxes and how problematic working with anonymous boxes can be, I'd say that the good rule of thumb would be the following:

Never place a text node without a wrapper in the block where other blocks are supposed to appear.

Just <li>Text</li> is completely OK. But if the nested list is supposed to be added to this <li> at some point in the future, IMHO, it would be better to wrap the text in any element — at least just <span> (then you would be able to set it display:block and any block properties, if such need suddenly occurs).

like image 31
Ilya Streltsyn Avatar answered Sep 30 '22 20:09

Ilya Streltsyn