htmlcssresponsive-designhtml-lists

Responsive Separator for Horizontal List


This question expands upon 'Separators For Navigation' by asking, how it is possible to remove the separators at the line breaks cause by viewport size.

Wide Viewport
->       Item 1 | Item 2 | Item 3 | Item 4 | Item 5       <-
Small Viewport
->  Item 1 | Item 2 | Item 3  <-
->      Item 4 | Item 5       <-

Here is a fiddle that shows how a pipe remains at the line break:

Fiddle.

I'm interested in a css-only solution, but javascript is acceptable if it provides the only possible solution.


Solution

  • Explanation

    You can exploit fact that trailing and line trailing white space automatically collapses:

    document.write(`<div>`
    + `word<b> </b>`.repeat(42)
    + `</div>`);
    b {
        background: red;
        outline: 1px solid blue;
    }
    div {
        resize: both;
        overflow: hidden;
    }

    sequence of words on three lines, with red rectangles between each two words

    As you can see, there are red spaces with blue outlines between words, but the very last and and those at line ends lack the red area, because its width collapsed to zero: that is the white-space collapsing in effect.

    It is possible to adjust width with word-spacing and use pseudo element instead, so setting ::after { content: ' '; word-spacing: 2em; } gives you wide inline rectangle that can display decorated backgrounds but disappears when not between two words on same line.

    Simplified example

    Simplified use case (from https://codepen.io/myf/pen/dyOzpZM, tested just in 2021-02 evergreen Firefox and Chromium, will not work in pre-Chromium Edge; for more robust example see the second snippet below):

    ul {
      text-align: center;
      padding: 0;
    }
    li {
      display: inline;
    }
    li::after {
      /*
       This has to be space, tab or other
       breakable white-space character:
      */
      content: " ";
      word-spacing: 1em;
      background-image: linear-gradient(
        -0.2turn,
        transparent 0 calc(50% - 0.03em),
        currentcolor 0 calc(50% + 0.03em),
        transparent 0
      );
    }
    /*
     That's it: just inline text
     with styled ::after spaces
     that collapse at line breaks
     and at the end of the element.
     
     That's basically how spaces work in text.
    */
    
    /*
     Unrelated whimsical effects:
    */
    body { background: #456; color: #fed; min-height: 100vh; margin: 0; display: flex; align-items: center; }
    ul { --dur: 3s; font-family: Georgia, serif; font-size: min(7vw, calc(100vh / 7)); margin: 0 auto; position: relative; padding: 0 1em; -webkit-text-fill-color: #999; text-transform: capitalize; animation: poing var(--dur) infinite alternate ease-in-out; }
    @keyframes poing { from { max-width: 3.4em; } to { max-width: min(19em, calc(100vw - 2em)); color: lime; } }
    ul::before, ul::after { -webkit-text-fill-color: currentcolor; position: absolute; top: 50%; transform: translatey(-50%); animation: calc(var(--dur) * 2) calc(var(--dur) * -1.5) infinite forwards linear; }
    ul::before { content: "☜"; left: 0; animation-name: a !important; }
    ul::after { content: "☞"; right: 0; animation-name: b !important; }
    @keyframes a { 50% { content: "☛"; } }
    @keyframes b { 50% { content: "☚"; } }
    ul:hover, ul:hover::before, ul:hover::after { animation-play-state: paused; }
    <ul>
     <li>foo</li>
     <li>bar</li>
     <li>baz</li>
     <li>gazonk</li>
     <li>qux</li>
     <li>quux</li>
    </ul>

    Two lines of metasyntactic variables with tall slashes between each two words when horizontally adjacent to each other. Manicule on both sides point towards the text block.

    It uses flat list with single-word items.

    More complex example with elements highlights

    nav {
      text-align: center;
      padding-right: 1em; /* = li::after@word-spacing */
    }
    ul {
      display: inline;
      margin: 0;
      padding: 0;
    }
    li {
      display: inline;
      /*
       white-space: nowrap should be moved to child A
       because IE fails to wrap resulting list completely
      */
    }
    li::before {
      content: ' ';
      /*
       this content is important only for Chrome in case
       the HTML will be minified with *no whitespaces* between </li><li>
      */
    }
    li::after {
      content: ' ';
      /*
       this is actual placeholder for background-image
       and it really must be space (or tab)
      */
      white-space: normal;
      word-spacing: 1em;
      /*
       = nav@padding-right - this actually makes width
      */
      background-image: radial-gradient(circle, black, black 7%, transparent 15%, transparent 35%, black 45%, black 48%, transparent 55%);
      background-size: 1em 1em;
      background-repeat: no-repeat;
      background-position: center center;
      opacity: 0.5;
    }
    /*
     no need to unset content of li:last-child::after
     because last (trailing) space collapses anyway
    */
    a {
      white-space: nowrap;
      display: inline-block; /* for padding */
      padding: 1em;
      text-decoration: none;
      color: black;
      transition-property: background-color;
      transition-duration: 500ms;
    }
    a:hover {
      background-color: #ccc;
    }
    /*
     For demonstrative purposes only
     Give items some content and uneven width
    */
    nav:hover > ul > li {
      outline: 3px dotted rgba(0,0,255,.5);
      outline-offset: -3px;
    }
    nav:hover > ul > li::after {
      opacity: 1;
      background-color: rgba(255, 0, 0, .5);
    }
    nav:hover > ul > li:hover {
      outline-style: solid;
    }
    nav:hover > ul > li:hover::after  {
      background-color: cyan;
    }
    
    nav:hover > ul > li > a {
      outline: 3px solid rgba(0,255,0,.5);
      outline-offset: -3px;
    }
    
    nav > ul {
      counter-reset: c;
    }
    nav > ul > li {
      counter-increment: c;
    }
    nav > ul > li > a::before {
      content: counter(c, upper-roman) '. ';
      letter-spacing: .3em;
    }
    nav > ul > li > a::after {
      content: ' item ' counter(c, lower-roman);
      word-spacing: .3em;
      letter-spacing: .1em;
      transform: translatex(.1em);
      display: inline-block;
    }
    <nav>
      <ul><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li><li><a href="#"></a></li>
      </ul>
    </nav>
    <!--  For demonstrative purposes is content of links made by CSS
    -->

    three lines of items, each with Latin number. Colourful outlines show various boundaries.

    (Originally from https://jsfiddle.net/vnudrsh6/7/) This proof-of-concept uses background-image of "eventually collapsing" CSS generated content space after each <li>. Tested in 2016 in Firefox, Chrome and IE11.


    You might need to use some character or more complex shape as divider (separator). Naturally you can use (vector) background-image, and you can even use text in SVG, although making it aligned to surrounding ("real") text might be challenging.

    Bare-bones with SVG

    Minimal working example without any "list" element, with textual fleuron:

    body {
      text-align: center;
    }
    b::after {
      content: " ";
      word-spacing: 16px;
      background: url("data:image/svg+xml;charset=utf-8,\
      <svg xmlns='http://www.w3.org/2000/svg' \
        viewBox='-3,-15,16,16'>\
          <text>❦</text>\
      </svg>");
    }
    div {
        resize: both;
        overflow: hidden;
    }
    <div>
    <b>foo</b> <b>bar</b> <b>baz</b> <b>gazonk</b> <b>qux</b> <b>quux</b> 
    <b>foo</b> <b>bar</b> <b>baz</b> <b>gazonk</b> <b>qux</b> <b>quux</b> 
    <b>foo</b> <b>bar</b> <b>baz</b> <b>gazonk</b> <b>qux</b> <b>quux</b>
    </div>

    metasyntactic variables separated with fleurons

    Justified block

    If you need to have spaced items making straight side lines producing justified text block, text-align-last in addition to text-align: justify may come in handy. Conveniently, it's the white space "character" yet again what makes the "stretching" adjustments here:

    ul {
      text-align: center;
      padding: 0;
      font-size: 30px;
      text-align: justify;
      text-align-last: justify;
      overflow: hidden;
      resize: both
    }
    
    li {
      display: inline;
    }
    
    li::after {
      content: " ";
      background-color: highlight;
      word-spacing: 3em;
      background-image: linear-gradient(
        0.25turn,
        transparent 0 calc(50% - 0.03em),
        mark 0 calc(50% + 0.03em), transparent 0
      );
    }
    
    html {
      color-scheme: light dark;
    }
    <ul>
      <li>foo</li>
      <li>bar</li>
      <li>baz</li>
      <li>gazonk</li>
      <li>qux</li>
      <li>quux</li>
    </ul>

    Two lines of metasyntactic variables each couple horizontally separated with highlighted rectangle with vertical line in the centre. Second line contains only two words, which are tuck to left and right side and wide separating rectangle fill remaining space.


    Other notable answers: