Skip to content

No script tags

Menus, modals, tabs and tooltips — in CSS.

Everything on this page is state held by the browser itself: a checkbox, an open attribute, the top layer, or the URL fragment. The server rendered it once and then stopped being involved.

Slide-out sidebar

body:has(#sidebar-toggle:checked)A visually hidden checkbox holds the open state, and :has() lifts that state up to the body so a panel elsewhere in the document can slide in — the parent selector replaces the script that used to toggle a class.

Esc-free, click the scrim to close

translate + transitionThe panel is always in the DOM at translate: -100%; checking the box moves it to 0 and the transition does the rest.

Accordions

details[open] .chevron<details> and <summary> are a disclosure widget with built-in keyboard support; the chevron rotation is just a rule matching the open attribute.

Is there really no JavaScript?
No script tags, no inline handlers, no external bundles. The only files the browser downloads are one HTML document, one stylesheet and the webfonts.
What happens in older browsers?
Details, checkboxes and :target have worked for years. Popover, anchor positioning and scroll-driven animations degrade to a still-usable page: the popover renders centred, the tooltip sits beside its trigger, and reveals show immediately.
Where does the state live?
In the DOM and the URL. The checkbox holds the sidebar, the open attribute holds the accordion, the top layer holds the popovers, and the fragment holds the tabs.

summary::-webkit-details-markerThe native triangle is removed with list-style: none so the custom chevron can take its place.

Dropdown menu

popover + :popover-openThe menu is a popover element opened by its invoker's popovertarget attribute; the browser promotes it to the top layer, closes it on Esc or an outside click, and returns focus — all behaviours that used to need a library.

position-anchor + position-areaAnchor positioning pins the menu under its button without measuring anything in JavaScript, and position-try-fallbacks flips it above when there is no room below.

Modal dialog

::backdropThe same popover primitive, styled as a centred panel with a dimmed ::backdrop pseudo-element; light dismiss and Esc-to-close come free with popover="auto".

@starting-styleThe fade-and-scale entrance uses @starting-style with transition-behavior: allow-discrete, so an element going from display: none animates in properly.

Tabs from the URL

:targetEach panel is hidden until the URL fragment matches its id, so tab state lives in the address bar and is shareable, bookmarkable, and survives a reload.

Each tab is an ordinary anchor pointing at a fragment, and each panel is a section with a matching id. Following the link changes the fragment, which changes what :target matches.

Panels are display: none by default and display: block when targeted. The active tab is highlighted with :has() — the tab strip checks whether the panel it points at is currently the target.

Fragment navigation adds a history entry and scrolls the panel into view, which is why the panels carry a scroll margin. In exchange, tab state is in the URL and survives a reload.

:not(:has(:target))The first panel is shown by default with a rule that only applies while no panel is targeted — the CSS equivalent of an initial state.

Tooltips

anchor-name / position-anchorThe tooltip is positioned against a named anchor instead of a wrapper with position: relative, so it can escape overflow: hidden and never needs measuring.

Positioned above its anchor with position-area: block-start.Same component, position-area: inline-end.

position-areaOne property picks the cell of the 3×3 grid around the anchor to sit in; swapping block-start for inline-end moves the tooltip with no other change.

Form controls and validation

:user-invalid:user-invalid only matches after the field has been interacted with and blurred, so the error appears when the user is done typing rather than the moment the page loads.

That does not look like an email address yet.

Looks good.

Notifications

appearance: noneThe checkbox and slider are native controls stripped back with appearance: none and rebuilt with ::-webkit-slider-thumb and :checked — they keep their keyboard behaviour.