Wrap indicator in <pre> blocks
I am not a front-end developer, not a UI designer, nor a UX guru, but I am an engineer, so when I face a puzzle worth solving my brain switches on and I cannot let it go until I find a satisfactory solution to the puzzle.
My blog heavily relies on me sharing session dumps and file excerpts using the code blocks. I am using PrismJS to highlight syntax in these blocks. However, after a while I found that there is one thing that really irritates me: these code blocks are not designed to be truly responsive, for instance – when a line wraps there is no easy to spot indication that such a wrap happened.
I quick search on the topic highlighted that this is kind of an unresolved issue and I found just one blog post from 2012 by Ian Yang after I already implemented my solution.
The approaches are quite close, but I like mine better since I think it is more
semantically correct (I am using <div>
instead of <span>
and I did a bit
deeper research into how to make it universal).
So, below I present you with my version of the solution and I would appreciate any feedback you may have, especially if it could help to find a nicer solution in the end.
Before we dive into details on how I come up with the solution, feel free to
play with the basic demo – try to resize the box by dragging the bottom right
corner and see how the text inside responds (if you are using anything than
the Chrome browser you may be out of luck with the resizable <div>
s, sorry):
This is quite a long line. It is long enough to ensure that it will wrap no matter how big your screen is. Well, it is possible that in 20 years from the moment I type this humanity will invent a medium that could display the whole line with no breaks, but until then it should be good enough for the demonstration purposes.A very short line.The short line above is used to demonstrate that it stays clear of additional info when its neighbour lines are wrapped.
I hope that demo gods were not angry at me and the demo above worked for you as expected, so if you want to understand how it works let’s dive under the hood of this solution :)
Preparation
First of all, we need to define what we have and what we want to achieve – it helps to stay on course, to understand when we reached a solution, and to assess how good the solution is:
- we have one or more code blocks expressed through the
<pre>
HTML element; - these code blocks may have continuous white space that we want to preserve;
- our page layout is flexible and there are no guarantees that the width of a particular viewport (a window through which a browser renders the visible part of the page) is enough to display the whole line of code;
- we want to ensure that layout does not break when a very long code line is encountered, so we expect that line to be wrapped to the next line upon hitting width of the containing box;
- we need to present the reader (a user who is consuming information) a visual indicator that the line was wrapped;
- we also want the syntax highlighter (such as PrismJS in my case) not to be affected by whatever we come up with.
I think the above quite a solid definition of the goals and requirements for our little project. It is time to assess the artefacts we have and what we can do with them.
Analysis
Our primary artefact is the <pre>
element. It contains the information we
want to share with the world and we want to do this in the most easy to digest
way.
By default, the <pre>
element is quite a simplistic container. Its semantic
meaning dictates that the content preserves the original formatting (such as
amount and type of white space on the lines). The element also provides some
basic functionality: you can define the size of the container and browsers are
supposed to present you with controls to scroll the content if it does not fit
the size of the container.
The following is just such bare almost not styled <pre>
block wrapped into a
resizable <div>
container, so you could play with the dimensions of the
container (in order to preserve the layout I am keeping the wrapping styles, so
the box would fit into the layout):
This is an example of a basic <pre> container. You should be able to resize it by dragging the bottom right corner (but if you are using anything other than Chrome you may be out of luck with the resizing). This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)
However, if you played with the above container you may have noticed that at some point all lines are so mixed up together and it is really hard to tell that the original text comprised of five separate lines.
Unfortunately, the CSS standards do not provide any selectors or
pseudo-elements to anchor the soft breaks. All we can do with the CSS
selectors on the contained text is to choose the first letter (with
:first-letter
) and the first line of the text inside the <pre>
element
(with :first-line
).
This is not good enough if you ask me. I would love to see some CSS selectors for soft breaks in the future iterations of the standard, but we are not there yet, so we have to work with what we have got.
And all we have is the standard CSS basic box model presentation focused
selectors such as ::before
and ::after
.
So, if we look how text is rendered inside the <pre>
block (browser’s
DevTools is a really good tool to do the observations, by the way) we will
notice that the browser considers the entire text as a single entity – there
are no lines or anything, just a blob of text.
Therefore, the first piece of the puzzle would be to introduce something that could make the distinction between different lines of text possible.
Assuming that we can solve the above issue with introducing lines, the next step would be to display a marker on the right side of the line. That marker should follow the height of the line box as the line wraps.
Finally, we will need to figure out how not to display the marker on the last line of the wrapped multiline text to denote the end of wrapped line.
Implementation
Since the definition of the <pre>
element only stipulates that the content of
the element is pre-formatted with white space we can use other tags inside,
e.g. we can wrap each physical line (a line that is terminated by a newline
character) in a block element such as <div>
.
Let’s see how it looks on our sample <pre>
block when we wrap each physical
line in <div class="line">
…</div>
. Keep in mind that <div>
is a block
element, so in order not to introduce unintended line breaks we need to ensure
that every newline character in the original text except the very first one is
replaced with the combination of the closing and opening tags such as
</div><div class="line">
, below is the result of such a change on our sample:
This is an example of a basic <pre> container. You shouldbe able to resize it by dragging the bottom right corner (but if you areusing anything than Chrome you may be out of luck with the resizing).This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)
Visually, it is almost exactly the same as the original sample we presented in the “Analysis” section, however there is a couple of differences.
The first one is visual and is obvious: the empty line (the fourth line of the
sample) has collapsed into nothing. This is an undesired side effect and the
reason for that is that the corresponding <div>
block has no content, hence
it has zero height. We can fix it by defining the min-height
attribute for
the <div>
s defining line boundaries.
The second difference is not so obvious, but if you inspect any line of this
<pre>
block using your browser’s DevTools you will see that now we can
differentiate between lines (when you walk through the DOM in your DevTools
your browser will highlight the corresponding line inside the <pre>
block and
this is what we wanted to achieve at this step!
The next step is to display a marker on the right side of the line and that
marker should occupy the exactly the same height as the line block we are
attaching it to. Well, this can be easily done with the ::after
pseudo-element:
pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
right: -1em;
background-color: blue; /* temporary, to make it visible */
}
Note that we also introduced position: relative
to our <div>
‘s. It is
needed since if we want to position our pseudo-element relative to its parent,
the parent needs to have the position attribute set to anything but static
.
In our context, relative
is the desired positioning for the <div>
element.
The corresponding rendered sample with the above stylesheet applied follows:
This is an example of a basic <pre> container. You shouldbe able to resize it by dragging the bottom right corner (but if you areusing anything than Chrome you may be out of luck with the resizing).This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)
So far so good! We have a blue coloured strip hanging on the right side just next to our text. Let’s style it a bit so it looks more appealing.
We have a couple of options here:
- we can just use a transparent bitmap image with the desired marker;
- we can leverage SVG as the source of the image.
The issue I have with transparent bitmap images is that they are not very responsive: to accommodate to all the possible resolutions I would end up with creating many bitmaps of different sizes to ensure that the marker looks good no matter what device is rendering the page.
On the other hand, the SVG graphics was not well supported on some browsers like Internet Explorer 6, etc. – The support is much better now, hence I would choose SVG every time.
Ideally, I did not want to use an image, but I could not figure out how I could put a repeating text block next to my lines, hence I found the following compromise (this is the content of the SVG file I created):
The first two lines are mandatory and describe to a browser that it works with an SVG image file.
The third line (viewBox=
) is really important since it allows the SVG image
to be scaleable, basically it says that the dimensions of the image are 25 by
25 units (the units are abstract, but you may think of them as pixels if it
makes it easier).
Finally, the <text>
element puts a text box with just a single Unicode
character for the return symbol (⏎), since I think it is quite an
adequate symbol to represent a line wrap point.
There are different ways how you can incorporate an SVG image into CSS rule, but since the content of the SVG file is so small and I did not want to host it externally (it would be an additional fetch request and more maintenance), I just did the following:
In other words, I removed all the white space, then deleted all line breaks, so
I got a single line SVG body, and I converted it to Base64. The resulting line
is usable as the inline definition of an image. Let’s update our stylesheet
with the new values (we just replaced the last line to change the background
to background-image
) and added dimensions for the background itself:
pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
right: -1em;
background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==);
background-size: 1em 1em;
}
This is an example of a basic <pre> container. You shouldbe able to resize it by dragging the bottom right corner (but if you areusing anything than Chrome you may be out of luck with the resizing).This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)
Look at that, it is almost shaping into what we want :). However, something is
not right – the alignment of the markers to the corresponding lines is nowhere
to be seen. It is, like, they are of different sizes despite that the
font-size
property for both of them is the same. Hmm.
If we recall that CSS basic model document we will discover in the very last paragraph that:
note that for non-replaced inline elements, the amount of space taken up (the contribution to the height of the line) is determined by the line-height property
Is this our hint? Let’s try to explicitly set the line-height
property for
our lines (reading documentation you may find that the default value is
normal
and you can also find that normal
equals 1.2
for the majority of
browsers, but relying on that, I think is a bad idea). We also need to adjust
our markers with the new information we have got:
pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
line-height: 1.2; /* set the height explicitly */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1.2em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
right: -1.2em;
background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==);
background-size: 1.2em 1.2em;
}
This is an example of a basic <pre> container. You shouldbe able to resize it by dragging the bottom right corner (but if you areusing anything than Chrome you may be out of luck with the resizing).This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)
This is an improvement, is not it? So we just need some final touches to call it done:
- As it stands, we are showing the marker for the last part of the wrapped line, but we should not;
- The colour of the marker is too bright and matches the text colour of the line wrapping of which we are highlighting.
To address the latter a simple play with the opacity
attribute will suffice,
e.g. I think 50% transparency should make the marker less distracting.
The former, however, requires some thinking. Given that the post is already too long, I will just state that out of multiple options I had I chose the following: raise the background image by one line-height
from the bottom, and clip a square 1x1 line-height
at the top to compensate, as follows:
pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
line-height: 1.2; /* set the height explicitly */
}
pre div.line::after {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1.2em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
bottom: 0;
right: -1.2em;
background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==);
background-size: 1.2em 1.2em;
margin-bottom: 0;
clip: rect(1.2em 1.2em 100vh 0);
opacity: .5;
}
The only tricky part in the added lines is the clip: rect(1.2em 1.2em 100vh
0)
line. The clip
property defines the visible rectangle the browser should
preserve, but there is a conundrum: the height of the wrapped line is variable,
so we cannot deterministically set this. The trick here is that it is highly
unlikely (on the border of completely impossible :) ) that the visible height
of the wrapped line would be bigger that the height of the viewport the user is
looking through.
Therefore, we are setting the height of the clipped area to the height of the
viewport (100vh
), but this will be a rare edge case to actually utilise this
– in the majority of cases the height of the clipping rectangle will be
limited by the height of the block containing the wrapped line. Basically,
this is exactly what we want :).
Let’s see the final result in action:
This is an example of a basic <pre> container. You shouldbe able to resize it by dragging the bottom right corner (but if you areusing anything than Chrome you may be out of luck with the resizing).This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)
Bonus Round
It should be mentioned, it is also possible to apply the same approach to the left side of the lines with a bit of a twist.
pre div.line {
position: relative; /* so we could position the child */
min-height: 1em; /* to avoid collapsing empty lines */
line-height: 1.2; /* set the height explicitly */
text-indent: -1.2em; /* first line indent */
padding-left: 1.2em; /* padding for the line */
}
pre div.line::before {
content: ''; /* we need content for pseudo elements */
position: absolute;
display: block;
width: 1.2em; /* without width it will be invisible */
height: 100%; /* to match the height of the parent */
top: 0;
left: 0;
background-image: url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij48dGV4dCB4PSIwIiB5PSIxOSI+JiN4MjNjZTs8L3RleHQ+PC9zdmc+Cg==);
background-size: 1.2em 1.2em;
clip: rect(1.2em 1.2em 100vh 0);
opacity: .5;
transform: rotateY(180deg); /* mirror the arrow */
transform-origin: left;
}
The trick here is the play between padding-left
and text-indent
for the
<div>
element since text-indent
will be applied to the first line only
(effectively negating the padding set for the whole element), thus the rest of
wrapped line will be padded to make space for the marker, which we place as
usual with the clipping region to suppress it for the very first part of the
wrapped line.
The result looks as follows (and I am actually torn which side I like better, so, maybe, for my blog I may just go with the left-side solution):
This is an example of a basic <pre> container. You shouldbe able to resize it by dragging the bottom right corner (but if you areusing anything than Chrome you may be out of luck with the resizing).This line was made specifically long to trigger the line wrapping, so even those who are using a browser that cannot resize should be able to play with it :)
This idea for the padding-left
and text-indent
trick came from C. Shaun
“Kainaw” Wagner when he answered a question about
the wrap indicators on Stack
Overflow (I am sure that it was known before, but this is where I learnt it).
Assessment of the result
We started with the following requirements:
-
these code blocks may have continuous white space that we want to preserve;
Achieved: we did some changes to ensure that even empty lines are preseved.
-
our page layout is flexible and there are no guarantees that the width of a particular viewport (a window through which a browser renders the visible part of the page) is enough to display the whole line of code
Achieved: our blocks are flexible and if a line is too long it is wrapped in an easy to understand way. We proved it by ensuring that any block can be dynamically resized.
-
we want to ensure that layout does not break when a very long code line is encountered, so we expect that line to be wrapped to the next line upon hitting width of the containing box;
Achieved: ditto as the previous item.
-
we need to present the reader (a user who is consuming information) a visual indicator that the line was wrapped;
Achieved: we got our dynamic markers showing when the line was wrapped.
-
we also want the syntax highlighter (such as PrismJS in my case) not to be affected by whatever we come up with.
Unknown: I think it would be the material for another post, but I do not expect a lot of issues since we only replaced the newline characters and preserved the rest of lines intact.iIt turned out, that it was quite a hassle since none of plugins PrismJS have for working with lines expect semantic markup of the lines inside the block. Therefore, I spent some time and came up with a plugin that does it “right”.
It is highly likely, that my plugin will get merged into the PrismJS tree (see the corresponding Pull Request).After a discussion with upstream, it seems they want to rework a large chunk of the project to use the proposed functionality and make it a library of some sort. I do not have time to actively see it through, so one of PrismJS developers will continue the integration of my idea into PrismJS and something comparable will be implemented in PrismJS in near future. However, if for some reason it would not be or you need the functionality “here & now”, you can always get it from my fork of PrismJS (I will keep theplugin-lines
branch untilit is merged upstreamupstream decides how to incorporate my idea).By the way, my Lines plugin is the primary working horse for all syntax highlighted blocks on this site (so you have already seen it in action).
I hope this helped you and you learnt something :). If you have any constructive feedback, I would appreciate it, specifically if you have ideas on how to improve the presented solution.