CSS and HTML are getting much more powerful
⚠️ NEW STUFF ⚠️
Standards-track APIs
<select name="version-select" id="version-select"
<button>
<selectedcontent></selectedcontent>
</button>
<option value="core">
<div class="icon">
<span class="material-symbols">hub</span>
</div>
<div class="meta">
<div class="title">UnAI Core</div>
<div class="subtitle">Balanced speed...</div>
</div>
</option>
<option value="max">
...
<div class="comments">
<a interestfor="commenters" href="#">
24 comments
</a>
</div>
<div id="commenters" popover="hint">
<ul>
<li>Jenny Smith</li>
<li>...</li>
</ul>
</div>
<div class="star-rating" data-rating="1.8">
<figure></figure>
</div>
.star-rating {
--percent-fill: calc(attr(data-rating type(<number>)) * 20%);
figure {
/* hard breakpoint gradient background */
background:
linear-gradient(to right,
gold var(--percent-fill),
transparent var(--percent-fill));
/* star mask */
mask-image: url('data:image/svg+xml,...');
}
&::after {
content: attr(data-rating);
}
}
The most exciting things happening in web development right now are happening in CSS & HTML.
The best way to level up your web development skillset is to know what's possbile with modern UI capabilities.
select,
select::picker(select) {
appearance: base-select;
}
<select name="version-select" id="version-select"
<button>
<selectedcontent></selectedcontent>
</button>
<option value="core">
<div class="icon">
<span class="material-symbols">hub</span>
</div>
<div class="meta">
<div class="title">UnAI Core</div>
<div class="subtitle">Balanced speed...</div>
</div>
</option>
<option value="max">
...
selectedcontent .subtitle,
selectedcontent .icon {
display: none;
}
--rad: calc(var(--btn-size) + var(--gap));
background-color: var(--bg);
transform: translate(
calc(cos(var(--angle)) * var(--rad)), /* x */
calc(sin(var(--angle)) * var(--rad))); /* y */
transition: transform 0.3s var(--delay) ease;
.item:nth-child(1) {
--bg: pink;
--angle: 0deg;
--delay: 0s;
}
.item:nth-child(2) {
--bg: thistle;
--angle: -45deg;
--delay: 0.1s;
}
.item:nth-child(3) {
--bg: paleturquoise;
--angle: -90deg;
--delay: 0.2s;
}
...
.item {
--radius: calc(var(--btn-size) + var(--gap));
--angle: calc((sibling-index() - 1) * -45deg);
--delay: calc((sibling-index() - 1) * 0.1s);
background: hsl(calc(sibling-index() * 40), 70%, 80%);
transform:
translate(
calc(cos(var(--angle)) * var(--radius)),
calc(sin(var(--angle)) * var(--radius))
);
transition: transform 0.3s var(--delay) ease;
}
@function --radial-coord(--a, --rad) {
result: translate(
calc(cos(var(--a)) * var(--rad)),
calc(sin(var(--a)) * var(--rad))
);
}
.item {
--radius: calc(var(--btn-size) + var(--extra-space));
--delay: calc((sibling-index() - 1) * 0.1s);
--angle: calc((sibling-index() - 1) * -45deg);
transform: --radial-coord(var(--angle), var(--radius));
background: hsl(calc(sibling-index() * 40), 70%, 80%);
transition: transform 0.3s var(--delay) ease;
}
@keyframes appear {
from {
opacity: 0;
transform: translate(0px, 50px);
}
to {
opacity: 1;
transform: translate(0px, 0px);
}
}
li {
animation: appear linear both;
animation-timeline: view();
animation-range: entry 0 cover 25%;
}
@supports (animation-timeline: scroll()) {
html {
scroll-timeline-name: --root-scroll;
}
@keyframes fade-in {
from {
opacity: 0;
display: none;
}
to {
opacity: 1;
display: block;
}
}
a {
animation: fade-in auto linear both;
animation-timeline: --root-scroll;
animation-range: 20% 30%;
}
}
<button popovertarget="my-tooltip">
<p aria-hidden="true">?</p>
<p class="sr-only">Open Tooltip</p>
</button>
<div id="my-tooltip" class="tooltip" popover>
<p>I am a tooltip with more information.<p>
</div>
#my-tooltip {
inset: auto;
position-area: top;
position-try: flip-block;
}
.tooltip {
container-type: anchored;
/* arrow */
&::before {
content: '';
top: 100%;
left: 50%;
border: solid transparent;
border-top-color: var(--tooltip-bg);
...
@container anchored(fallback: flip-block) {
bottom: 100%;
top: calc(-2 * var(--spacer));
border-top-color: transparent;
border-bottom-color: var(--tooltip-bg);
}
}
<!-- Invoker -->
<button popovertarget="my-tooltip">
<p aria-hidden="true">?</p>
<p class="sr-only">Open Tooltip</p>
</button>
<!-- Popover -->
<div id="my-tooltip" class="tooltip" popover>
<p>Lavender and fuchsia danced...</p>
</div>
<!-- Invoker -->
<button interestfor="my-tooltip">
<p aria-hidden="true">?</p>
<p class="sr-only">Open Tooltip</p>
</button>
<!-- Popover -->
<div id="my-tooltip" class="tooltip" popover>
<p>Lavender and fuchsia danced...</p>
</div>
<p>Hi! My name is <a href="https://una.im"
interestfor="--una">Una Kravets</a>.
I'm a CSS and UI enthusiast...</p>
<div id="--una" popover="hint">
<header>
<picture>
<img src="una.jpg" alt="Una">
</picture>
<div>
<h2>una</h2>
<a href="https://una.im">una.im</a>
</div>
<button>Follow</button>
</header>
</div>
[interestfor] {
interest-delay-start: 0.3s;
}
.parent:has(:interest-source) [interestfor] {
interest-delay-start: 0s;
}
Protip from Emil Kowalski —>
popover=auto |
popover=manual |
popover=hint |
|
---|---|---|---|
Light dismiss (via click-away or esc key) |
✅ Yes | 🚫 No | ✅ Yes |
Closes other popover=auto elements when opened |
✅ Yes | 🚫 No | 🚫 No |
Closes other popover=hint elements when opened |
✅ Yes | 🚫 No | ✅ Yes |
Closes other popover=manual elements when opened |
🚫 No | 🚫 No | 🚫 No |
Can open and close popover with JS (showPopover() or hidePopover() ) |
✅ Yes | ✅ Yes | ✅ Yes |
Default focus management for next tab stop | ✅ Yes | ✅ Yes | ✅ Yes |
Can hide or toggle with popovertargetaction |
✅ Yes | ✅ Yes | ✅ Yes |
Can open within parent popover to keep parent open |
✅ Yes | ✅ Yes | ✅ Yes |
[interestfor]
Polyfill
import interestfor from "https://esm.sh/interestfor";
[interestfor] {
/* for polyfill this must be a custom property */
--interest-delay-start: 0.1s;
--interest-delay-end: 0.1s;
interest-delay-start: var(--interest-delay-start);
interest-delay-end: var(--interest-delay-end);
}
<ul class="carousel">
<li>...</li>
<li>...</li>
<li>...</li>
</ul>
.carousel {
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
}
.carousel {
&::scroll-button(*) {
position: fixed;
position-anchor: --carousel;
font-family: "Material Symbols Outlined";
}
&::scroll-button(right) {
position-area: inline-end center;
content: "arrow_forward_ios" / "Scroll Right";
}
&::scroll-button(left) {
position-area: inline-start center;
content: "arrow_back_ios" / "Scroll Left";
}
}
.carousel {
scroll-marker-group: after;
&::scroll-marker-group {
position: fixed;
position-anchor: --carousel;
display: grid;
...
}
::scroll-marker {
content: counter(day-num);
aspect-ratio: 1 / 1;
...
}
::scroll-marker:target-current {
transform: translateY(-0.5rem);
}
}
li {
container-type: scroll-state;
scroll-snap-align: center;
div {
scale: 0.8;
transition: scale 0.3s ease;
@container scroll-state(snapped: x) {
scale: 1;
}
}
}
.day-card {
& .meta {
transform: translateY(calc(100% - 3rem));
@container scroll-state(snapped: x) {
transform: translateY(0);
}
}
}
::scroll-marker:target-current {
anchor-name: --active-target;
}
.indicator {
position-anchor: --active-target;
inset: anchor(inside);
margin-top: anchor-size();
transition: inset 0.5s linear;
}
.follower {
/* anchor the follower element */
position: fixed;
position-anchor: --hovered;
}
.possible-anchor:hover {
/* update the active anchor */
anchor-name: --hovered;
}
Multi-anchors!
1. Dynamic re-targetting for "bubble"
2. Ephemeral tooltips using:
position-area
position-try-fallbacks
popover=hint
[interestfor]
(Touch of JS to preserve active state since we're
re-anchoring with :hover)
scroll-target-group: auto
<ul class="parent">
<li><a href="#intro">Introduction</a></li>
<li><a href="#one">Section one</a></li>
</ul>
<div id="intro">Introduction</div>
<div id="one">Section one</div>
.parent {
scroll-target-group: auto;
}
:target-current {
/* styles for active anchor */
}
if()
shape()
Range container queries
VIEW TRANSITIONS
/* Show only one of two */
#feedback-form, .open #feedback-btn {
display: none;
}
/* Give both the same v-t-name, because */
/* we want to morph the one into the other */
#feedback-form, #feedback-btn {
view-transition-name: --feedback;
}
/* Capture the parent as a separate layer, */
/* so it animates too */
#parent {
view-transition-name: --feedback-parent;
}
...
<style contenteditable>
font-family: 'FontWithASyntaxHighlighter'
::scroll-button(*)
::scroll-marker
& :target-current
scroll-behavior: smooth
@container scroll-state(snapped: x) {}
sibling-index()
The best way to level up your web development skillset is to get good at CSS and HTML
Leverage the power of the browser
Raise your expectations for the web platform.