Responsive Separator for Horizontal List
Asked Answered
P

5

19

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.

Panhandle answered 5/5, 2016 at 14:1 Comment(2)
Possible duplicate of CSS: Last element on lineKiser
I didn't actually find this question prior to writing my question. However, I feel this question is intending slightly different answers and use cases. Also, couldn't this redux provide solutions that align with current specs.Panhandle
C
29

Explanation

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

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 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 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, tall slashes between each two words. Manicule on both sides.

It uses flat list with single word items, so is not very relevant for real-world usage.

More realistic 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 colapsing" CSS generated content space after each <li>. Tested in 2016 in Firefox, Chrome and IE11.


Obviously you might need to use some character or more complex shape as divider. Naturally you can use (vector) background-image, and you can even use text in SVG, although making it correspond with surrounding ("real") text might be quite daunting.

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>");
}
<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>

metasyntactic variables separated with fleurons


Other notable answers:

  • Same technique used in overlooked Liphtier's answer from 2014. (I've found that one long after posting this answer, so to my disappointment I cannot claim my answer is was first.)
  • Same technique used few months later in Tom Robinson's answer.
  • Impressive flex-box-based solution with plain over-extending borders and different spacing in gfullam's answer.
  • For left-aligned list you can set overflow hidden and cut overlapping real character in pseudo element: Oriol's answer and Nathan Arthur's. .
Cully answered 5/5, 2016 at 14:41 Comment(13)
Hummm... I like that. Let us see what others can come up with too.Panhandle
It still would be nice to have a solution that would work with other characters (ie. bullets, right angle quote, etc.).Panhandle
Mine works with JS by adding/removing classes, so you might wanna check that out.Offstage
@Panhandle true, and in fact I meant this more as an amusing hack, but but as it turns out, it could be developed further to include possibility for background images for collapsing separators. But with another drawback; See updated answer.Cully
@Cully I'm particularly interested in this solution because it is pure css. Because this is intended to server mobile devices, I don't want to use resize handlers if it is at all possible.Panhandle
@Panhandle ok, polished the proof-of-concept so it seems usable for any arbitrary separator background-image. Haven't tested on mobile devices though. TBH quite surprised how well it turned out :]Cully
Nice! The text was off center by the amount of the separator. Would if be correct to add nav@padding-left to compensate? jsfiddle.net/vnudrsh6/8 In this example made the size bigger and added backgrounds to make the alignment more visiblePanhandle
Sure! I think that nav@padding could be omitted completely if not needed for other reasons; the one for the right side was only necessary for previous variations (if at all) and prevailed more by my own misunderstanding.Cully
@Cully Okay. Here is my final update as I have implemented it. I've tested Chrome, Firefox, IE11, iOS, and Chrome Mobile. jsfiddle.net/vnudrsh6/9Panhandle
I don't understand how this is an answer to the question which requested a CSS-only solution. This answer is adding a load of otherwise unneeded elements to the DOM (ie it's an HTML plus CSS solution).Curate
@AHaworth I've added bare-bones non-semantic example for demonstration, could you point out what elements you identify as "unneeded" there? The question is about "list" and such structure usually involves some semantic wrapper element for item elements. I cannot fancy how could "CSS-only solution" for problem that inherently involves (HTML) content look like.Cully
My mistake - I didn't read things carefully enough to realise the 'simplified' solution was so different - apologies.Curate
Understood. Still, I don't see the HTML structure of the "realistic" example much different - it is basically the same as bare-bones body > b, just transposed to idiomatic navigation structure nav > ul > li > a, that was implied in the question. I don't think there is much to take out of there without compromising semantics and usability.Cully
H
4

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)

Halcyon answered 5/5, 2016 at 14:34 Comment(3)
This risks failing to align the the parent-level elements because -4px is just an estimate of how wide " | " will be, right?Cognizant
Why not do div ul li:not(:first-child)::before { content: " | "; } on the last line of CSS? Then you don't have to do the 4px trick.Counterespionage
I thought that is a nice solution, but it doesn't work with a centered menu. @JoomyKorkut not(:first-child) doesn't work after the line break.Slipnoose
C
2

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: '';  
  }
}

}
Carycaryatid answered 5/5, 2016 at 14:17 Comment(1)
No. The element I will ultimately be using this technique on will only have a max-width.Panhandle
R
1

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

Romain answered 5/5, 2016 at 14:18 Comment(2)
Nice! the problem with this its not fluid responsive meaning that it wont show up after lets say portrait to mobile(sor instance) but if its not an issue gg!Oestradiol
@Oestradiol good point. There's a lot of eventualities to take into account with this one... Very interesting question.Romain
O
0

My implementation with JavaScript: https://jsfiddle.net/u2zyt3vw/5/

Hit "Run" again after you've resized the window.

You can also add event listeners such as onresize. Here's the JS:

var listItems = document.getElementsByTagName("li");
var listItemsWidth = [];
var listItemsDistance = [];

for (let i = 0; i < listItems.length; i++) {
  listItemsWidth[i] = listItems[i].offsetWidth;
  listItemsDistance[i] = listItems[i].getBoundingClientRect().right;
}

for (let i = 0; i < listItems.length; i++) {
  if (listItemsDistance[i] == Math.max.apply(null, listItemsDistance)) {
    listItems[i].classList -= "notLast";
  } else {
    listItems[i].classList = "notLast";
  }
}

I added the notLast class to all of your elements, and that's what contains the :after pseudo-element with the pipe. This script removes this class from the ones that are closer to the right edge of the container.

I also messed around with the :after pseudo-element and made it position:absolute; for dark reasons.

Offstage answered 5/5, 2016 at 15:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.