Modern HTML & CSS in 2026
For years, my default for anything interactive was a React or Vue project with a pile of packages on top. A lot of that code was there to patch gaps in HTML and CSS. In 2026 a good number of those gaps are closed, and I keep deleting JavaScript from my own projects because the browser can do the job now. This post is my list so far.
View transitions
Single-page apps were partly an answer to full page loads that flashed a white screen. If I wanted a smooth fade between two views, I needed a client-side router and an animation library.
The View Transitions API moves that into the browser. In a single-page app, document.startViewTransition() animates between two DOM states. In a multi-page site, one CSS rule gives same-origin navigations a crossfade:
@view-transition {
navigation: auto;
} If an element has the same view-transition-name on both pages, the browser moves it from one position to the other. That’s how post titles on this blog slide from the list into the article header. Cross-document support still differs between browsers, so check MDN before you depend on it. A browser without support loads the page the old way, which is fine.
:has() and container queries
Styling a parent based on its children used to need a JavaScript listener, because CSS selectors could only look down the tree. :has() lifts that limit. Container queries let a component respond to the width of its container instead of the whole viewport.
/* The card responds to its own width, not the viewport */
.card {
container-type: inline-size;
}
/* Cards that contain an image get a different surface */
.card:has(img) {
background: var(--surface-subtle);
border: 1px solid var(--border-highlight);
}
@container (width > 30rem) {
.card-body {
display: grid;
grid-template-columns: 1fr 2fr;
}
} I can drop the same card into a narrow sidebar or a wide grid and the CSS picks the layout, with no script measuring widths.
Update, 2026-10-20
Since writing this I ripped GSAP and bits-ui out of this site. The mobile menu is now an HTML popover, so the browser handles Escape, the outside click, and the backdrop. The contact form uses a native <select>, styled with appearance: base-select in browsers that support it. The page intro and the scroll reveals on the blog list are CSS keyframes and animation-timeline: view(). Two fewer dependencies to update, and less JavaScript for your phone to download.