CSS Demo Gallery (2026)
Animate to height: auto
Can I use: interpolate-sizeCan I use: @starting-style
Animating an accordion open once required JavaScript to measure scrollHeight and set an explicit pixel height. Two properties remove that work. interpolate-size: allow-keywords permits transitions to and from intrinsic sizes such as auto, while @starting-style defines the value a property should animate from. Applied to a <details> element, the panel animates on its own.
What is rendered server-side?
.accordion {
interpolate-size: allow-keywords;
}
details::details-content {
height: 0;
opacity: 0;
transition:
height 0.35s ease,
opacity 0.35s ease,
content-visibility 0.35s allow-discrete;
}
details[open]::details-content {
height: auto;
opacity: 1;
}@starting-style makes 'animate in' a real thing
@starting-style defines the values an element animates from the first time it renders, which finally gives you a real enter animation in pure CSS. Each chip below starts transparent and offset, then settles into place the moment it is added. There is no requestAnimationFrame toggle and no class swapped in on the next frame.
.chip {
transition:
opacity 0.4s ease,
transform 0.4s ease;
}
@starting-style {
.chip {
opacity: 0;
transform: translateY(12px) scale(0.9);
}
}scrollbar-gutter: stable ends the layout jump
When a page switches between scrolling and not scrolling, the scrollbar appears and disappears, and the content jumps sideways by its width. scrollbar-gutter: stable reserves that space at all times, so the layout stays put. This one affects the page's own scrollbar, so there is nothing to embed here. Add both-edges if you want the gutter mirrored on both sides.
html {
scrollbar-gutter: stable;
}Type that scales itself, sensibly
Can I use: clamp()Can I use: container query units
The pattern to remember is clamp(min, preferred, max). Viewport units scale with the whole window, which is wrong for a heading inside a sidebar or card. The cqi unit measures one percent of the nearest container's inline size instead, so the type tracks its own column. Drag the corner of the box below and the heading resizes even though the window stays fixed.
Resize this box.
clamp(1.25rem, 0.8rem + 6cqi, 3rem). Drag the bottom-right corner.
.card {
container-type: inline-size;
}
.card h1 {
font-size: clamp(1.25rem, 0.8rem + 6cqi, 3rem);
}Components that lay themselves out
Container queries let a component react to the width of its own container instead of the viewport. Mark the parent with container-type: inline-size, then write @container rules against its width. Drag the panel below from narrow to wide and the card switches from stacked to a row. It needs no media query and knows nothing about the page around it.
Self-arranging card
Stacked when the container is narrow, a row once it passes 420px, set by the box, not the window.
.wrapper {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
}
@container (min-width: 420px) {
.card {
flex-direction: row;
align-items: center;
}
}Native popovers, no library
Add the popover attribute and an id to an element, then point a button at it with popovertarget. The browser takes care of top-layer stacking, click-outside dismissal, and the Escape key. This demo positions the panel with CSS anchoring and animates it using @starting-style plus allow-discrete, which lets it fade and slide even though display and the top layer normally change instantly.
<button popovertarget="menu">Show details</button>
<div id="menu" popover>Anchored with pure CSS.</div>
<style>
button { anchor-name: --menu-btn; }
#menu {
margin: 0;
inset: auto;
position-anchor: --menu-btn;
position-area: bottom span-right;
margin-top: 8px;
opacity: 0;
transform: translateY(-6px) scale(0.96);
transition:
opacity 0.25s ease,
transform 0.25s ease,
overlay 0.25s allow-discrete,
display 0.25s allow-discrete;
}
#menu:popover-open {
opacity: 1;
transform: translateY(0) scale(1);
}
@starting-style {
#menu:popover-open {
opacity: 0;
transform: translateY(-6px) scale(0.96);
}
}
</style>A real autocomplete with <datalist>
<datalist> attaches a native suggestion dropdown to a normal text input. The user can type anything, and the browser renders and filters the matching options as they go. When you need suggestions from a known list but still want to allow free text, this covers it in one element with no script.
<input list="stacks" />
<datalist id="stacks">
<option value="Server-side rendering"></option>
<option value="Islands architecture"></option>
</datalist>CSS anchor positioning, the Floating UI killer
Anchor positioning ties one element to another and keeps them aligned through scrolling and resizing, handled entirely by the browser. Name an element with anchor-name, then place another relative to it using the anchor() function. Together with the Popover API and @position-try fallbacks, it covers what positioning libraries were doing before.
.trigger {
anchor-name: --btn;
}
.tooltip {
position: absolute;
position-anchor: --btn;
top: anchor(bottom);
left: anchor(left);
}Styling a sticky header when it sticks
Can I use: scroll-state queries
Scroll-state container queries let CSS know when a sticky element is currently pinned, so a header can pick up a shadow only after it sticks. Set container-type: scroll-state on the wrapper and query scroll-state(stuck: top). Scroll the panel below and the header stays flat at rest, then gains a shadow the instant it sticks. No IntersectionObserver required.
.header-wrap {
container-type: scroll-state;
position: sticky;
top: 0;
}
@container scroll-state(stuck: top) {
.header {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
}Themes without the boilerplate: light-dark()
The light-dark() function puts both theme values in a single declaration. Set color-scheme on the root, then write each color as light-dark(lightValue, darkValue). There is no separate prefers-color-scheme block to keep in sync. Across a design system with dozens of tokens, that saves a lot of duplicated declarations.
light-dark() call per property. Switch your OS or browser theme and it follows.:root {
color-scheme: light dark;
}
.card {
background: light-dark(#f4eff4, #2a2a3e);
color: light-dark(#1e1e32, #e0e0e6);
}A toggle switch from a checkbox
The switch attribute renders a checkbox as a native toggle: <input type="checkbox" switch>. It stays a real checkbox for forms and assistive tech, but it looks like the switch you would otherwise assemble from a hidden input and a couple of divs. Browser support is still landing, so use it as progressive enhancement. Where it is missing, you get a standard checkbox.
<input type="checkbox" switch checked />Scroll-driven animations with animation-timeline
A scroll animation used to mean an IntersectionObserver or a scroll listener driving an animation frame by frame. Now you link a keyframe animation directly to a scroll position with animation-timeline. Use view() to run an animation based on where an element sits in the viewport, or scroll() to tie one to a scroll container's progress. The bar at the top fills as you scroll the panel, and each card fades in as it enters, all off the main thread with no script.
Scroll this panel ↓
@keyframes reveal {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
/* A reading-progress bar tied to the scroll position. */
.progress {
animation: grow linear;
animation-timeline: scroll();
transform-origin: left;
}
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}A stylable <select> with appearance: base-select
Can I use: appearance: base-select
The native <select> has always been almost impossible to style, which is why every design system ships its own from scratch. Setting appearance: base-select on the control and its ::picker(select) opts into a fully stylable version. You can put arbitrary markup inside each <option>, and <selectedcontent> mirrors the chosen option into the button. It is still a real form control with native keyboard behavior. Support is early, so treat it as progressive enhancement over a normal select. See the Chrome team's writeup, The <select> element can now be customized with CSS, for the full anatomy.
select,
::picker(select) {
appearance: base-select;
}
::picker(select) {
border: 1px solid #d0d0d8;
border-radius: 12px;
padding: 6px;
}
option {
display: flex;
gap: 10px;
border-radius: 8px;
padding: 8px 10px;
}
option:checked { font-weight: 600; }
/* Rich content is allowed inside options now. */
<select>
<button>
<selectedcontent></selectedcontent>
</button>
<option value="ssr">🖥️ Server-side rendering</option>
<option value="ssg">📄 Static generation</option>
</select>Conditional logic in a value with if()
The if() function brings real conditional logic into a property value. You can branch on a media query, a supports() test, or the value of a custom property with style(), and fall through to an else branch. The three badges below share one rule and pick their color from a --tone custom property, so the styling logic lives in CSS instead of a Sass mixin or an inline calculation.
<span class="badge" style="--tone: danger">danger</span>
<span class="badge" style="--tone: success">success</span>
<span class="badge">default</span>
<style>
.badge {
/* Pick a color from the --tone custom property, with a fallback. */
background: if(
style(--tone: danger): #c0362c;
style(--tone: success): #1a7f37;
else: #555
);
}
</style>Staggered animations with sibling-index()
Can I use: sibling-index()Can I use: sibling-count()
To stagger a list before, you hand-wrote a delay for :nth-child(2), :nth-child(3), and so on, or generated them in a preprocessor. Now sibling-index() gives each element its position and sibling-count() gives the total, so a single calc(sibling-index() * -0.12s) offsets every item in the list no matter how many there are. Here each row runs the same looping animation, shifted by its index, so the motion ripples down the list.
- Item 1
- Item 2
- Item 3
- Item 4
- Item 5
- Item 6
- Item 7
- Item 8
li {
/* A looping wave. Each item's negative delay offsets it in the
cycle by its position, so the animation ripples down the list. */
animation: wave 1.6s ease-in-out infinite;
animation-delay: calc(sibling-index() * -0.12s);
}
@keyframes wave {
0%, 100% { transform: translateX(0); opacity: 0.55; }
50% { transform: translateX(16px); opacity: 1; }
}Reading typed values from HTML with attr()
Can I use: attr() type support
The old attr() only produced strings, useful for content and little else. It can now read an attribute as a typed value: pass a data attribute through type(<color>) or type(<length>) and use it anywhere that type is valid. The chips below read their fill straight from a data-color attribute and the bars read their width from data-width, so the data can live in your HTML or come from a CMS without a line of JavaScript.
<span class="chip" data-color="#2ea043">#2ea043</span>
<span class="chip" data-color="#4c6ef5">#4c6ef5</span>
<div class="bar" data-width="40%"></div>
<div class="bar" data-width="90%"></div>
<style>
.chip {
/* Parse the data-color attribute as a real color, with a fallback. */
background: attr(data-color type(<color>), #986088);
}
.bar {
/* And data-width as a real length. */
inline-size: attr(data-width type(<length>), 0px);
}
</style>Generating shades with color-mix() and relative colors
Can I use: color-mix()Can I use: relative colors
You no longer need fifty hand-picked shades in your root variables. Define one base token and derive the rest. color-mix() blends two colors in a chosen color space, so color-mix(in oklch, var(--primary) 20%, white) gives you a tint, and mixing toward black gives a shade. Relative color syntax goes further, letting you pull a base color apart and adjust a single channel. Every swatch below comes from one --primary token.
:root {
--primary: oklch(0.62 0.16 300);
}
.tint { background: color-mix(in oklch, var(--primary) 20%, white); }
.base { background: var(--primary); }
.shade { background: color-mix(in oklch, var(--primary) 80%, black); }
/* Relative color syntax: derive from a base and tweak one channel. */
.faded { background: oklch(from var(--primary) l c h / 30%); }