For this tutorial you’ll need Chrome 65+ or Firefox 59+. Take a look at the accompanying GitHub repo for the the step-by-step code. We’ll leverage the following features to create a better experience and work around some issues.
CSS Display Module (Level 3)
- display: contents;
- W3C status – Working Draft
- Browser support
CSS Conditional Rules Module (Level 3)
- @support(…) {…}
- W3C status – Candidate Recommendation
- Further reading: MDN web docs
CSS Overscroll Behavior Module (Level 1)
- overscroll-behavior: contain;
- W3C status – Unofficial
- Browser support
- Further reading: Take control of your scroll
CSS Selectors Module (Level 4)
- :focus-within
- W3C status – Working Draft
- Browser support
- Further reading: MDN web docs :focus-within, :placeholder-shown
The CSS Containment Module (Level 1)
- contain: paint;
- W3C status – Candidate Recommendation
- Browser support
- Further reading: CSS Containment in Chrome 52
01. Set up the HTML for the newsfeed
First we need to set up some super simple, repeating markup for our newsfeed. Let’s create a .container div with an unordered list inside. Give the <ul> the class of .feed, then create 10 list-items each containing a div with the .card class and the text Card 1, Card 2 etc.
Finally create another list-item in-between 5 and 6 with a class of .nested – this will be helpful later – and add a <ul> inside with three list items using the text Card A, Card B etc.
<body>
<div class="container">
<ul class="feed">
<li><div class="card">Card 1</div></li>
<li><div class="card">Card 2</div></li>
<li><div class="card">Card 3</div></li>
<li><div class="card">Card 4</div></li>
<li><div class="card">Card 5</div></li>
<li class="nested">
<ul>
<li><div class="card">Card A</div></li>
<li><div class="card">Card B</div></li>
<li><div class="card">Card C</div></li>
</ul>
</li>
<li><div class="card">Card 6</div></li>
<li><div class="card">Card 7</div></li>
<li><div class="card">Card 8</div></li>
<li><div class="card">Card 9</div></li>
<li><div class="card">Card 10</div></li>
</ul>
</div>
</body>
02. Style the newsfeed
Now we need to add some quick styles so that this starts to look more like a newsfeed. First we can give <body> a subtle grey background colour. Then give .container a max-width of 800px and use margin: 0 auto; to centre align it.
Let’s also give .card a white background, 10px of padding and margin and finally a min-height of 300px – this should give us enough to make the page scrollable. Lastly we’ll sprinkle some Flexbox magic on the .feed to make the items flow nicely, with two cards per row.
.feed {
display: flex;
flex-wrap: wrap;
}
.feed li {
flex: 1 0 50%;
}
03. Fix layout problems
If you scroll down the list you’ll notice that our cards in the nested list, Card A – C, are causing some layout problems. Ideally we’d like them to flow in with the rest of the cards but they are all stuck together in one block. The reason for this is that a flex container – which is created using display: flex – only makes its immediate children (i.e. the list items) into flex items.
Now, normally the only way of fixing this is to change the markup, but let’s pretend that you don’t have that luxury. Perhaps the newsfeed markup is generated by a third-party script or it’s legacy code that you’re only trying to reskin. So how can we fix this?
Meet display: contents. This little one-liner essentially makes an element behave as if it isn’t there. You still see the descendants of the element but the element itself doesn’t affect the layout.
Because we’re pretending we can’t change the markup (just for this step) we can be a bit smart about this and make the .card elements the flex items and almost entirely ignore the list markup.
First remove the existing .feed li class and then use display: contents for both <ul> and <li> elements:
.feed ul,
.feed li {
display: contents;
}
Now you should see the cards flowing in order, but we’ve lost the sizing. Fix that by adding the flex property to the .card instead:
.card {
flex: 1 0 40%;
}
Ta-da! Our cards are now using the wonders of Flexbox as if their structural unordered list markup doesn’t exist.
As a side note you might be wondering why the flex-basis value is set to 40%. This is because the .card class has a margin, which isn’t included in the width calculation as you would expect when using box-sizing: border-box. To work around this we just need to set the flex-basis high enough to trigger the wrapping at the necessary point and Flexbox will fill the remaining space automatically.
04. Explore feature queries
Although display: contents does exactly what we need, it is still only at Working Draft status with W3C. And in Chrome support only arrived in version 65 released in March 2018. Incredibly Firefox has had support since April 2015!
If you disable the style in DevTools you’ll see that our changes have made a bit of a mess with the layout when display: contents isn’t applied. So what can we do about this? Time for our next new feature – feature queries.
These work just like media queries but they allow you to ask the browser – using CSS alone – if a property and value expression is supported. If they are, the styles contained inside the query will be applied. Now, let’s move our display: contents styles into a feature query.
@supports (display: contents) {
.feed ul,
.feed li {
display: contents;
}
.card {
flex: 1 0 40%;
}
}
05. Use ‘not’ for a cleaner outcome
Normally in this kind of progressive enhancement scenario we’d use the query to add the new styles, but it would also have to disable some of the original styles necessary for the fallback layout.
However you might decide that because support for feature queries is pretty good (if you ignore IE) you actually want to use the feature query not operator. It works just like you’d expect, so we can re-apply our original flex property to the list-items when display: contents is not supported:
@supports not (display: contents) {
.feed li {
flex: 1 0 50%;
}
}
Inside the not query we can add some styles so that the .nested items basically re-apply what was being inherited by using display: contents.
feed li.nested {
flex-basis: 100%;
}
.feed li.nested ul {
display: flex;
flex-wrap: wrap;
}
06. Taking it one step further
You can already see the potential of feature queries, but the really cool thing is that you can combine expressions using the three available operators: and, or and not. Perhaps we could also check for display: flex support and then add a float-based fallback.
We’re not going to do that here, but if we were we’d first modify the two feature queries like so:
@supports (display: flex) and (display: contents) {
...
}
@supports (display: flex) and (not (display: contents)) {
...
}
As a bonus you can also test for custom properties support like this:
@supports (--foo: green) {
...
}
07. Add a chat box
Now we have a beautiful newsfeed in place, let’s add a little chat box that is fixed to the bottom right of the screen. We’ll need a list of messages and a text field for the user to enter their message. Add this block just after the opening <body> tag:
<div class="chat">
<div class="messages">
<ul>
<li><div class="message">Message 1</div></li>
<li><div class="message">Message 2</div></li>
<li><div class="message">Message 3</div></li>
<li><div class="message">Message 4</div></li>
<li><div class="message">Message 5</div></li>
<li><div class="message">Message 6</div></li>
<li><div class="message">Message 7</div></li>
<li><div class="message">Message 8</div></li>
<li><div class="message">Message 9</div></li>
<li><div class="message">Message 10</div></li>
</ul>
</div>
<input type="text" class="input">
</div>
08. Style the chat box
Time to quickly add some styling so it looks half decent.
.chat {
background: #fff;
border: 10px solid #000;
bottom: 0;
font-size: 10px;
position: fixed;
right: 0;
width: 300px;
z-index: 10;
}
.messages {
border-bottom: 5px solid #000;
overflow: auto;
padding: 10px;
max-height: 300px;
}
.message {
background: #000;
border-radius: 5px;
color: #fff;
margin: 0 20% 10px 0;
padding: 10px;
}
.messages li:last-child .message {
margin-bottom: 0;
}
.input {
border: none;
display: block;
padding: 10px;
width: 100%;
}
09. Scroll chaining
Hopefully now you’ll have a little chat box with a scrollable list of messages sitting on top of your newsfeed. Great. But have you noticed what happens when you scroll inside a nested area and you reach the end of its scrollable range? Try it. Scroll all the way to the end of the messages and you’ll see the page itself will start scrolling instead. This is called scroll chaining.
It’s not a big deal in our example but in some cases it might be. We’ve had to work around this feature before when creating scrollable areas inside modals or context menus.
The dirty fix is to set the <body> to overflow: hidden, but there is a nice, shiny new CSS property that fixes all of this and it’s a simple one-liner. Say hello to overscroll-behavior. There are three possible values:
- auto – The default, which allows scroll chaining
- contain – Disables scroll chaining
- none – Disables scroll chaining and other overscroll effects (e.g. rubberbanding)
We can use the shorthand overscroll-behavior or we can target a specific direction with overscroll-behavior-x (or -y). Let’s add it to our .messages class:
.messages {
...
overscroll-behavior-y: contain;
... }
Now try the scrolling again and you’ll see that it no longer affects the page’s scroll when you reach the end of the content.
This property is also pretty handy if you wanted to implement a pull-to-refresh feature in your PWA, say to refresh the newsfeed. Some browsers, such as Chrome for Android, automatically add their own and until now there has been no easy way to disable it without using JS to cancel events using performance-impacting non-passive handlers.
Now you just need to add overscroll-behavior: contain to the viewport-containing element, probably <body> or <html>.
It’s worth noting that this property is not a W3C Standard, rather a proposal by the Web Incubator Community Group (WICG). Popular, stable and mature WICG proposals are considered for migration to a W3C Working Group at a later stage.
10. Collapse the chat box when not in use
At the moment the chat box takes up quite a bit of space, which unless we’re interacting with it is a bit distracting. Fortunately we can do something about this with a little CSS magic.
But first of all we need to modify our existing styles slightly. By default we want the chat box to be collapsed, so we need to reduce the max-height value in the .messages class. While we’re there we can also add a transition to the max-height property:
.messages {
...
max-height: 25px;
transition: max-height 500ms; }
11. Expand the chat box when it receives focus
So our chat box is currently collapsed and unusable. How can we expand it using only CSS? Time for a new feature – the Focus Container pseudo-class :focus-within.
This is just like the old faithful :focus pseudo-class selector, but it matches if any of the element’s descendants have focus. This is interesting because it is the reverse of how CSS usually works, where you can only select elements based on their ancestors. It opens up some interesting potential.
Adding this class will set the max-height of the .messages element to 300px, when anything inside the .chat area receives focus. Remember that an element must accept keyboard or mouse events, or other forms of input, in order to receive focus. Clicking in the <input> will do the job.
.chat:focus-within .messages {
max-height: 300px; }
12. Take focus further
This is nice, but what else can we do? How about blurring the newsfeed when the chat box has focus? Well with the help of the subsequent-sibling combinator (~) we can do this really easily because the markup for the chat box is before the newsfeed markup:
.chat:focus-within ~ .container {
filter: blur(5px); }
That’s all it takes! We can soften the effect slightly by adding a transition on the filter property to the original .container class. Now we’re not suggesting this is a good idea, but we think it’s pretty cool that we can do this with CSS alone.
13. Explore placeholder-shown
There are a number of new pseudo-classes described in CSS Selectors Module Level 4. Another interesting one is :placeholder-shown, which matches any input that is showing placeholder text. At the moment our text field doesn’t have any placeholder text, so let’s add some.
<input type="text" class="input" placeholder="Enter your message">
And then immediately after the text field add a helper message to show the user.
<div class="prompt">Press enter to send</div>
Now add some styling for the helper message, so that by default it is collapsed.
.prompt {
line-height: 2em;
max-height: 0;
overflow: hidden;
padding: 0 10px;
text-align: right;
transition: max-height 500ms; }
14. Make the prompt visible
At the moment the prompt is hidden, so how can we use :placeholder-shown here? Well (most) browsers will display the placeholder text until the field has a value. Just setting focus won’t hide the placeholder. This is helpful because we don’t want the user to send a blank message anyway, so we can hook into this behaviour to show the prompt only when the user has entered a value.
Of course in the real world we’d need some validation too. We can use the negation pseudo-class :not to flip the meaning of :placeholder-shown and show the prompt when the placeholder text is not shown (i.e. the field has a value).
.input:not(:placeholder-shown) + .prompt {
max-height: 2em; }
Setting the max-height to double the 10px font-size, using 2em, will expand the block and the text becomes visible. Neat. Expect to see some clever uses for this seemingly mundane pseudo-class if it makes it through to the final spec.
15. Bring it all to life
We’ve built the bare bones of a newsfeed with a chat feature using some of the new properties coming to CSS, but at the moment it is lifeless – you can’t actually do anything with it. One particularly interesting new feature is CSS Containment, but it is difficult to demo without modifying the DOM. Let’s add some super-simple JS to allow us to add messages to our chat box.
First of all we need to add an ID to the <input> and the messages <ul>. We can also add a required attribute to the input too.
<ul id="messages" ...
<input type="text" id="input" required ...
Then create an empty JS file called script.js and add it to the page just before the closing </body> tag so we don’t have to wait for DOM ready.
<script src="script.js"></script>
</body>
16. Add a sprinkle of JavaScript
We need to add an event handler to the <input>, which listens for an Enter keypress, validates the field, takes the value (if valid) and adds it to the end of the messages list, clears the field and scrolls to the bottom of the messages. Something like this will do the job for our demo but don’t use this in production!
// Finds the important elements
const input = document.getElementById(‘input’);
const messages = document.getElementById(‘messages’);
// Listens for keypress events on the input
input.addEventListener(‘keypress’, (event) => {
// Checks if the key pressed was ENTER
if (event.keyCode === 13) {
// Checks the field is valid
if (input.validity.valid) {
// Creates a DOM element using the value
const message = createMessage(input.value);
// Appends the new message to the list
messages.appendChild(message);
// Clears the field
input.value = ‘’;
// Scrolls the messages to the bottom
messages.parentNode.scrollTop = messages.parentNode.scrollHeight;
}
}
});
// Converts value to string of HTML
function createMessage (value) {
// Warning: Don’t do this in production without sanitizing the string first!
return stringToDom(`<li><div class=”message”>${value}</div></li>`)
}
// Converts string to real DOM
function stringToDom (string) {
const template = document.createElement(‘template’);
template.innerHTML = string.trim();
return template.content.firstChild; }
Now when you type in the field and press Enter you’ll see your message added to the bottom of the element’s list.
17. Add some additional info
In order to demonstrate one of the benefits of the final new CSS property contain we need to do something a little contrived. We’re going to implement a feature that displays the time a new message was sent in a box at the top of the messages list. This will appear when you hover your mouse over a message.
First up we need to add this information to our new messages. We can modify the return line of the createMessage function to add the current time to an attribute.
return stringToDom(`
<li>
<div class="message message--mine" data-timestamp="${new Date().toString()}">
${value}
</div>
</li>
`);
You might have noticed that new messages have an additional class message–mine. Let’s add a quick class so they stand out and align to the right of the list.
.message--mine {
background: #ff2089;
margin-left: 20%;
margin-right: 0;
}
18. Display the timestamp
So our goal is to make the message’s timestamp appear at the top of the list of messages, but we need to do it so it is always visible even when the list is scrolled. First off let’s try and render the value using a pseudo-element. We only want to do this for our messages. See below.
.message--mine::after {
content: attr(data-timestamp);
}
That should display the timestamp inside the message. Let’s modify it slightly so it only appears on :hover, has a bit of styling and is fixed to the top of the messages area so it is visible even when scrolled.
.message--mine:hover::after {
background: #000;
color: #ff2089;
content: attr(data-timestamp);
left: 0;
padding: 5px;
position: fixed;
top: 0;
width: 100%;
}
Hmm, it sort of works but it is appearing at the top of the page rather than the scrollable area. Let’s add position: relative to the .messages pane to try and contain it.
.messages {
...
position: relative;
...
}
Ah, it doesn’t work either because fixed positioning is anchored to the viewport, not its relative ancestor. Time for our final new property – contain.
19. Explore Containment
CSS Containment is an exciting new proposition. It has a number of options that give you the ability to limit the browser’s styling, layout and paint work to a particular element. This is of particular benefit when modifying the DOM. Some changes – even small ones – require the browser to relayout or repaint the entire page, which obviously can be an expensive operation even though browsers work hard to optimise this for us.
Using CSS Containment we can essentially ring-fence parts of the page and say ‘what happens in here, stays in here’. This also works the other way around and the ring-fenced element can be protected from changes made outside.
There are four values for contain each with a different purpose. You can combine them as you require.
- layout – Containing element is totally opaque for layout purposes; nothing outside can affect its internal layout, and vice versa
- paint – Descendants of the containing element don’t display outside its bounds
- style – Certain properties don’t escape the containing element (sounds like the encapsulation you get with the Shadow DOM, but isn’t)
- size – Containing element can be laid out without using the size of its descendants
Alternatively you can use one of two keywords as a kind of shorthand:
- strict is an alias for ‘layout paint style size’
- content is an alias for ‘layout paint style’
Each of these values are a little opaque so I would recommend reading the spec and playing around with them in DevTools to see what actually happens.
The two important values are layout and paint as they offer performance optimisation opportunities when used within complex web apps that require a lot of DOM manipulation. In our rudimentary demo however we can leverage one of the consequences of using contain: paint to help us with the timestamp positioning.
According to the spec, when using paint the “element acts as a containing block for absolutely positioned and fixed positioned descendants”. This means we can set contain: paint on the .chat class and the fixed positioning will be based on the card rather than the viewport. You’d get the same effect by using transform: translateZ(0) but containment feels less hacky.
.chat {
...
contain: paint;
... }
20. Wrap it up
That concludes our whistle-stop tour of some new CSS features. Most of them are pretty straightforward, although we’d definitely recommend delving into CSS Containment in more detail. We’ve only really touched on a small part of it here and it is the kind of feature that sounds like it does something that it actually doesn’t – the encapsulation of styling. That said it could prove very helpful, and it is already at the Candidate Recommendation stage, so it’s well worth a look.
This article was originally published in creative web design magazine Web Designer.