加载中...
加载中...
Modern CSS features I now reach for in every project — and the specific moments on the Templately admin and sapan.dev where each one replaced a chunk of JavaScript or some BEM gymnastics that had been there for years.
On the Templately admin redesign last year, I deleted a class-toggling helper that had been carried since the original v1. It existed only because CSS could not select a parent based on its descendants. Then :has() shipped, and the whole helper became three CSS rules. Most of the modern CSS features below have a story like that — they replace something we used to write JavaScript or BEM gymnastics for.
Modern CSS has quietly absorbed most of what we used to need JavaScript for. Parent selectors, style scoping, and container-aware layouts are no longer roadmap items — they are shipping in every major browser. If your CSS still looks like 2020, you are writing more code than you need to.
The :has() pseudo-class lets you style an element based on its descendants. This was the single most-requested CSS feature for over a decade. It unlocks patterns that previously required JavaScript class toggling.
/* Style a form that has an invalid input */
form:has(input:invalid) {
border-color: var(--color-danger);
}
/* Style a card differently when it contains an image */
.card:has(img) {
padding: 0;
}
/* Style a list item based on its sibling */
li:has(+ li:hover) {
opacity: 0.6;
}
/* Theme the entire page based on a checkbox */
html:has(input[name="theme"]:checked) {
--bg: #111;
--text: #eee;
}信息
:has() is NOT slow. Browser engines optimize it heavily — it runs in constant time for most real-world queries. The "performance concerns" from 2020 no longer apply.
The @scope at-rule limits a block of styles to a subtree of the DOM. It replaces the awkward BEM conventions and nested selectors that global CSS requires for isolation. Styles apply only within the scope boundary and bleed no further.
/* Styles only apply inside .card, no further */
@scope (.card) {
h3 {
font-size: 1.25rem;
font-weight: 600;
}
p {
color: var(--secondary-600);
}
}
/* Lower bound — style stops at .nested-component */
@scope (.widget) to (.nested-component) {
button {
background: var(--brand);
}
}Media queries respond to the viewport. Container queries respond to a parent element. This matches how components actually work — a card in a sidebar should render differently than the same card in a hero section, regardless of the browser window size.
.card-container {
container-type: inline-size;
container-name: card;
}
/* Stack vertically when container is narrow */
@container card (max-width: 400px) {
.card {
flex-direction: column;
}
.card-image {
width: 100%;
}
}
/* Horizontal layout when container has space */
@container card (min-width: 600px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}Native nesting removes one of the last reasons to reach for a preprocessor like Sass. The syntax matches CSS Nesting Module Level 1 — slightly different from Sass but close enough that muscle memory transfers.
.button {
padding: 0.5rem 1rem;
background: var(--color-primary);
&:hover {
background: var(--color-primary-dark);
}
&.large {
padding: 1rem 2rem;
}
& .icon {
margin-right: 0.5rem;
}
}Anchor positioning lets absolutely-positioned elements reference other elements as their anchor. This replaces entire JavaScript libraries like Floating UI for tooltips, popovers, and menus. The browser handles collision detection and repositioning automatically.
.tooltip-trigger {
anchor-name: --trigger;
}
.tooltip {
position: absolute;
position-anchor: --trigger;
top: anchor(bottom);
left: anchor(center);
translate: -50% 8px;
/* Flip if it would overflow the viewport */
position-try-options: --top-flip;
}
@position-try --top-flip {
top: auto;
bottom: anchor(top);
translate: -50% -8px;
}提示
Combine container queries with :has() for truly reactive components — a card that changes layout based on whether it contains an image AND based on available width.
All features discussed here are supported in current Chrome, Safari, Firefox, and Edge. Anchor positioning is the newest — stable in Chromium 125+, Safari 18.4+, and Firefox 128+. For production apps, feature-detect the newest additions and provide layout fallbacks for the rest.
Across sapan.dev and the Templately admin redesign, every one of these features pulled its weight. :has() killed off the class-toggling helper. @scope let me drop a BEM convention the team had been carrying for years. Container queries finally let card components stop checking the viewport for layout decisions. Native nesting let me delete Sass from sapan.dev entirely — the build pipeline is one less step. If your CSS still looks like 2020, it is more verbose than it needs to be.
CSS的更多内容
Notes from rebuilding sapan.dev with the View Transitions API for navigation across 16 locales. Replaced an entire Framer Motion orchestration layer with a CSS file, found one annoying flash on RTL Arabic, and walked away with a much smaller bundle.
sapan.dev runs on Tailwind v4 — no `tailwind.config.js`, the design tokens live in CSS, and the build is dramatically faster. Notes from the migration off v3 and what the new architecture actually changes day-to-day.
sapan.dev's design tokens are oklch all the way down. The brand color is one variable; every shade, hover, and disabled state derives from it programmatically. Notes on what oklch and color-mix actually buy you in a design system.