HTML · CSS · Accessibility

Modern HTML & CSS

What the web platform does natively today, with no framework and no build step. Every demo below is plain HTML and CSS; the few lines of JavaScript are marked, and each demo still works, or degrades gracefully, in browsers that lack a feature.

Dialogs & popovers

A modal dialog and an anchored popover, opened by buttons with no click handlers. The browser handles focus trapping, Esc, light-dismiss and the top layer.

  • <dialog>
  • Popover API
  • Invoker commands
  • Anchor positioning

Dialog result: not opened yet

Publish this page?

Focus is trapped inside, the rest of the page is inert, and Esc or a click outside closes it. The button you press becomes the dialog's returnValue.

Keyboard shortcuts

Tab
Move to the next control
Shift + Tab
Move back
Esc
Close this popover
How it works
<button commandfor="demo-dialog" command="show-modal">Open dialog</button>
<dialog id="demo-dialog" closedby="any">
  <form method="dialog"> … <button value="publish">Publish</button> </form>
</dialog>

<button popovertarget="demo-popover">Keyboard shortcuts</button>
<div id="demo-popover" popover> … </div>

/* Tether the popover to its button */
.pop-trigger   { anchor-name: --pop-trigger; }
.demo-popover  { position-anchor: --pop-trigger; top: anchor(bottom); left: anchor(left); }

Exclusive accordion

Native <details> elements that share a name behave like a radio group: opening one closes the others. Where supported, they animate to their natural height.

  • <details name>
  • ::details-content
  • interpolate-size
Do I need JavaScript for this?

No. Keyboard support, the open/closed state and the screen-reader announcements all come from the element itself.

Is it searchable?

Yes. The browser's find-in-page (Ctrl + F) opens a closed section when it contains a match.

What happens in older browsers?

They ignore name, so several sections can be open at once, and they skip the animation. The content is still fully usable.

How it works
<details name="faq" open><summary>…</summary> …</details>
<details name="faq"><summary>…</summary> …</details>

.accordion details               { interpolate-size: allow-keywords; }
.accordion details::details-content { block-size: 0; transition: block-size .3s, content-visibility .3s allow-discrete; }
.accordion details[open]::details-content { block-size: auto; }

Forms without a framework

Built-in validation that only complains after you've touched a field, suggestions from a <datalist>, a slider with a live <output>, and a textarea that grows as you type. Nothing is sent anywhere.

  • :user-invalid
  • <datalist>
  • <output>
  • accent-color
  • field-sizing

At least 3 characters, please.

That doesn't look like a full URL.

10
Options

Fill in the required field to continue. Looks good. Ready to submit.

How it works
/* Errors appear only after the visitor has interacted with the field */
.field:has(:user-invalid) .field-error { display: block; }

/* The status line reacts to the whole form's validity: no JS */
.demo-form:has(:invalid)      .when-valid   { display: none; }
.demo-form:not(:has(:invalid)) .when-invalid { display: none; }

textarea { field-sizing: content; }      /* grows with its content */
.demo-form { accent-color: var(--demo-accent); }

:has(), the parent selector

Style an element based on what it contains. The selected plan card, the explanation below it and the checklist's progress are all driven by CSS reacting to form state.

  • :has()
  • CSS counters
Choose a plan

Starter: one project, community support. Pro: unlimited projects and a custom domain. Team: everything in Pro, plus shared workspaces.

Launch checklist

All done. Ship it!

How it works
.plan:has(:checked) { border-color: var(--accent); }
.has-plans:has([value="pro"]:checked) .note-pro { display: inline; }

/* Count checked boxes with CSS counters, celebrate when none are left unchecked */
.checklist { counter-reset: done; }
.checklist input:checked { counter-increment: done; }
.checklist-count::after { content: counter(done) " of 4 done"; }
.checklist:not(:has(input:not(:checked))) .checklist-done { display: block; }

Subgrid & container queries

Subgrid lines up the titles, text and buttons of sibling cards even when their content lengths differ. Container queries let a component respond to its own width rather than the window's.

  • subgrid
  • Container queries
  • cqi units
  • Short title

    A little text.

    Action
  • A much longer title that wraps onto two lines

    Its body is longer too, yet the buttons below still share one baseline with the other cards.

    Action
  • Medium title here

    Rows are defined once on the parent grid and shared with every card.

    Action

Drag the bottom-right corner of the dashed box to resize it (desktop), or use the slider.

Keyvan Ghasemirad

UX Designer & Front-End Lead

Demos
7
Frameworks
4

Layout: narrow (stacked)medium (side by side)wide (with stats)

How it works
.subgrid-demo { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
.sub-card     { display: grid; grid-row: span 3; grid-template-rows: subgrid; }

.cq-frame { container-type: inline-size; resize: horizontal; }
@container (min-width: 26rem) { .profile { grid-template-columns: auto 1fr; } }
@container (min-width: 40rem) { .profile-stats { display: flex; } }

Colour & typography

Every colour on this site is a light-dark() pair, so a single color-scheme declaration flips a whole region, as the two panels below show. Tints and shades are mixed in the perceptual OKLCH space, and headings balance their lines.

  • light-dark()
  • color-mix()
  • OKLCH
  • text-wrap: balance

Tints: color-mix(in oklch, var(--accent) N%, white)

Shades: color-mix(in oklch, var(--accent) N%, black)

color-scheme: light

Same tokens, same markup.

Button

color-scheme: dark

Same tokens, same markup.

Button

text-wrap: wrap

Designing dashboards that are readable at a glance

text-wrap: balance

Designing dashboards that are readable at a glance

How it works
:root          { --surface: light-dark(#ffffff, #171a21); }
.scheme-dark   { color-scheme: dark; }   /* every token inside flips */
.swatches li   { background: color-mix(in oklch, var(--accent) var(--p), white); }
h1, h2, h3     { text-wrap: balance; }

Motion

The progress bar at the very top of this page and the tiles below are driven by scroll position, not timers or scroll listeners. New list items fade in using @starting-style. All of it is switched off when your system asks for reduced motion.

  • @starting-style
  • prefers-reduced-motion
  • Scroll-driven animations
  • View Transitions
  • Scroll
  • driven
  • reveal
  • with
  • view()
  • timelines

    Also try the theme button in the header: it cross-fades with the View Transitions API, and moving between pages on this site does the same.

    How it works
    .read-progress { animation: grow linear both; animation-timeline: scroll(root); }
    @keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
    
    .reveal-grid li { animation: reveal linear both; animation-timeline: view();
                      animation-range: entry 0% cover 35%; }
    
    .toast-list li { transition: opacity .4s, translate .4s; }
    @starting-style { .toast-list li { opacity: 0; translate: -1rem 0; } }

    Semantic content

    The right element carries meaning for screen readers, search engines and reader modes. This table lists every feature on the page; its header stays visible while you scroll it.

    • <table> with caption & scope
    • <meter> / <progress>
    • <time>, <abbr>, <kbd>, <mark>
    Platform features used on this page
    Feature Type Support Demo
    <dialog>HTMLBaselineDialogs
    Popover APIHTMLBaselineDialogs
    Invoker commandsHTMLEnhancement + fallbackDialogs
    Anchor positioningCSSEnhancementDialogs
    <details name>HTMLBaselineAccordion
    ::details-contentCSSEnhancementAccordion
    :user-invalidCSSBaselineForms
    field-sizingCSSEnhancementForms
    :has()CSSBaseline:has()
    SubgridCSSBaselineLayout
    Container queriesCSSBaselineLayout
    light-dark()CSSBaselineColour
    color-mix()CSSBaselineColour
    text-wrap: balanceCSSBaselineColour
    @starting-styleCSSBaselineMotion
    Scroll-driven animationsCSSEnhancementMotion
    View TransitionsCSS + JSEnhancementMotion

    Press Ctrl + F to search this page. The W3C and WHATWG maintain these standards; this page was last updated on . Highlighted text uses <mark>.

    72% 45%

    The power of the Web is in its universality. Access by everyone regardless of disability is an essential aspect.

    Tim Berners-Lee, W3C Director and inventor of the World Wide Web

    Like what you see?

    These are the same techniques behind every page on this site. I'd be glad to bring them to your project.

    Get in touch
    Back to top