Keyvan Ghasemirad
UX Designer & Front-End Lead
- Demos
- 7
- Frameworks
- 4
HTML · CSS · Accessibility
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.
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 result: not opened yet
Keyboard shortcuts
<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); }
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.
No. Keyboard support, the open/closed state and the screen-reader announcements all come from the element itself.
Yes. The browser's find-in-page (Ctrl + F) opens a closed section when it contains a match.
They ignore name, so several sections can be open at once, and they skip the animation. The content is still fully usable.
<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; }
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.
/* 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); }
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.
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!
.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 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.
Drag the bottom-right corner of the dashed box to resize it (desktop), or use the slider.
UX Designer & Front-End Lead
Layout: narrow (stacked)medium (side by side)wide (with stats)
.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; } }
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.
Tints: color-mix(in oklch, var(--accent) N%, white)
Shades: color-mix(in oklch, var(--accent) N%, black)
text-wrap: wrap
Designing dashboards that are readable at a glance
text-wrap: balance
Designing dashboards that are readable at a glance
: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; }
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.
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.
.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; } }
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.
| Feature | Type | Support | Demo |
|---|---|---|---|
<dialog> | HTML | Baseline | Dialogs |
| Popover API | HTML | Baseline | Dialogs |
| Invoker commands | HTML | Enhancement + fallback | Dialogs |
| Anchor positioning | CSS | Enhancement | Dialogs |
<details name> | HTML | Baseline | Accordion |
::details-content | CSS | Enhancement | Accordion |
:user-invalid | CSS | Baseline | Forms |
field-sizing | CSS | Enhancement | Forms |
:has() | CSS | Baseline | :has() |
| Subgrid | CSS | Baseline | Layout |
| Container queries | CSS | Baseline | Layout |
light-dark() | CSS | Baseline | Colour |
color-mix() | CSS | Baseline | Colour |
text-wrap: balance | CSS | Baseline | Colour |
@starting-style | CSS | Baseline | Motion |
| Scroll-driven animations | CSS | Enhancement | Motion |
| View Transitions | CSS + JS | Enhancement | Motion |
Press Ctrl + F to search this page. The
W3C and WHATWG
maintain these standards; this page was last updated on .
Highlighted text uses <mark>.
The power of the Web is in its universality. Access by everyone regardless of disability is an essential aspect.
These are the same techniques behind every page on this site. I'd be glad to bring them to your project.
Get in touch