Back to Hub
UI PERFORMANCE •

CSS Grew Up. Your App UI Should Too.

CSS is no longer "the styling language." In the last few years it gained typed custom properties, container queries, native view transitions, programmatic highlight control, and a paint API that runs in a worker. If you're still writing app UIs like it's 2019, you're leaving real capability on the table.

Every example below is pure, runnable CSS/JS — paste it into NitroIDE's live preview and see it work instantly, no build step.

Typed custom properties with @property

Regular custom properties are dumb strings — the browser can't animate --progress: 0% to --progress: 100% because it doesn't know it's a percentage. @property registers the type, unlocking smooth animation of anything:

@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
.spinner {
  background: conic-gradient(from var(--angle), #ec4899, #8b5cf6, #ec4899);
  animation: spin 2s linear infinite;
}
@keyframes spin { to { --angle: 360deg; } }

Without @property, that keyframe does nothing — the browser can't interpolate an untyped string. With it, you get GPU-friendly animated gradients with zero JavaScript.

Paint with code: the Houdini Paint API

Need a background pattern CSS can't express — wavy dividers, confetti, dynamic grids? The CSS Paint API lets you draw with canvas-like code in a worklet that runs off the main thread:

// confetti-paint.js — registered as a paint worklet
registerPaint('confetti', class {
  paint(ctx, geom) {
    for (let i = 0; i < 60; i++) {
      ctx.fillStyle = `hsl(${i * 6}, 80%, 60%)`;
      ctx.fillRect(Math.random() * geom.width,
        Math.random() * geom.height, 6, 6);
    }
  }
});
// usage:
CSS.paintWorklet.addModule('confetti-paint.js');
.party { background: paint(confetti); }

Honest note: Houdini's grand vision (layout, animation worklets) mostly stalled — but the Paint API shipped in Chromium and is genuinely useful for generative backgrounds and dynamic patterns.

Container queries: components that respond to their box

Media queries respond to the viewport. Container queries let a component respond to its parent's size — which is what you actually want for reusable cards, sidebars, and dashboard widgets:

.card-wrapper { container-type: inline-size; }
.card { display: grid; grid-template-columns: 1fr; }
@container (min-width: 480px) {
  .card { grid-template-columns: 200px 1fr; }
}

Drop the same card in a narrow sidebar and a wide main column — it adapts to each context automatically. This is the single biggest layout upgrade for component-based UIs in years.

Native page transitions

The View Transitions API gives you animated transitions between DOM states with a few lines — no animation library:

document.startViewTransition(() => {
  renderNewPage(); // DOM updates happen inside
});
/* Opt elements into smooth morphing: */
.hero-image { view-transition-name: hero; }

Elements with matching view-transition-name values morph between states automatically. For multi-page apps, @view-transition { navigation: auto; } enables the same effect across full page loads in Chromium — genuinely app-like navigation with one CSS rule.

Performance note: view transitions snapshot the old and new states as images and animate those — the DOM itself isn't animating. Keep view-transition-name usage targeted; naming hundreds of elements can spike memory during the transition.

Style without strings: CSS Typed OM

Setting styles via strings (el.style.opacity = '0.5') forces the browser to parse text on every write. The Typed OM works with real numeric values — faster in tight loops like drag handlers and scroll effects:

// String parsing on every frame — slow:
el.style.transform = `translateX(${x}px)`;
// Typed values, no parsing — fast:
el.attributeStyleMap.set('transform',
  new CSSTranslate(CSS.px(x), CSS.px(0)));

Programmatic highlighting: the Custom Highlight API

::selection only covers user selections. The Custom Highlight API lets JavaScript highlight arbitrary ranges — the foundation of fast, custom syntax highlighting that doesn't fight the DOM:

const range = new Range();
range.setStart(textNode, 0);
range.setEnd(textNode, 9);
CSS.highlights.set('search-match', new Highlight(range));
::highlight(search-match) {
  background-color: #ec4899;
  color: white;
}

Rendering huge lists: content-visibility

Rendering 10,000 file rows crushes layout. content-visibility: auto tells the browser to skip rendering off-screen subtrees entirely until they're scrolled near:

.file-row {
  content-visibility: auto;
  contain-intrinsic-size: auto 48px; /* reserve space so scrollbars don't jump */
}

This single declaration can take a heavy list from seconds of layout to near-instant. The contain-intrinsic-size is essential — without it, the scrollbar thumb jitters as rows get measured.

Style at the Speed of Thought.

Try every technique above in a live preview — no PostCSS, no config, just CSS.

Launch NitroIDE