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.


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

document.write(
 'word<b style="background: red; outline: 1px solid blue;"> </b>'
 .repeat(42)
);

As you can see there are red spaces with blue outlines between words, but the very last and and two at line ends lack the red area because it's 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 inline ::after { content: ' '; word-spacing: 2em; } gives you wide inline rectangle that can have decorated backgrounds or borders but disappears when it is not between words.

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>

It uses flat list with single word items, so is not very relevant for real-world usage. More realistic variation suitable for navigation could be something like https://jsfiddle.net/vnudrsh6/7/ :

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
-->

This proof-of-concept uses background-image of "eventually colapsing" CSS generated content space after each <li>. Tested in 2016 in Firefox, Chrome and IE11.


Other notable answers:

  • This overlooked answer from 2014, predating mine.
  • Same trick used here.
  • Very impressive alternative using flex-box - plain over-extending borders and different spacing due flex arrangement.

A different solution from that same CSS: Last element on line seems like it would work here.

HTML:

<div>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
</div>

CSS:

div {overflow: hidden; margin: 1em; }
div ul { list-style: none; padding: 0; margin-left: -4px; }
div ul li { display: inline; white-space: nowrap; }
div ul li:before { content: " | "; }

(Fiddle)


If you have static width of your element you can calculate by the media-screen. If not use script

body {
  text-align: center;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  display: inline-block;

  &:not(:last-child):after {
  content: ' |';  
  }
}
@media screen and (max-width: 265px) {
  li {
  display: inline-block;

  &:not(:last-child):after {
  content: '';  
  }
}

}

Nice question. For the life of me, I can't think of a water-tight CSS-only solution I'm afraid...

I've modified an old solution to a similar question posted a while back: CSS: Last element on line. Funnily enough I was looking for a solution to another problem I had a while back and stumbled across this - been bookmarked since!

Here's a fiddle with my updates: https://jsfiddle.net/u2zyt3vw/1/

HTML:

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
  <li>Item 5</li>
</ul>

CSS:

body {
  text-align: center;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

li {
  display: inline-block;

  &:not(:last-child):after {
    content: ' |'

  }

}
li.remove:after {
  content: none;
}

jQuery:

$(window).on("resize", function () {
    var lastElement = false;
    $("ul > li").each(function() {
        if (lastElement && lastElement.offset().top != $(this).offset().top) {
            lastElement.addClass("remove");
        }
        lastElement = $(this);
    }).last().addClass("remove");
}).resize();

NOTE - it works best onload at the moment, resizing causes a few issue even if I use toggleClass(). So keep pressing "Run" every time you resize the view. I'll work on it and get back to you..