PHP Sort Multidimensional Array by Value (usort, Tested)
usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.
The HTML details and summary elements give you a working accordion with no JavaScript at all. Put the always-visible heading inside summary, put the hidden content after it inside details, and the browser handles clicking, keyboard support (Enter and Space) and the open or closed state for you. Add the open attribute to show a panel by default, and give several details elements the same name attribute to make them behave like a classic accordion where opening one closes the others. You can remove the default triangle with list-style: none on the summary (plus ::-webkit-details-marker for older Safari), replace it with your own icon, and even animate the opening with a few lines of modern CSS.
I built and tested the accordion in this guide in headless Chrome 154 on 7 October 2026, using a script that clicks the panels, presses keys and reads what a screen reader would announce. That test found one real trap, a "+" sign that gets read out as part of every heading, and I show the one-line fix. Everything else here is plain HTML and CSS you can paste into any page, including a WordPress or admin editor that strips scripts.
Here's the smallest possible example:
<details>
<summary>What is your returns policy?</summary>
<p>You can return unused items within 30 days for a full refund.</p>
</details>
The browser shows only the summary line, with a small triangle in front of it. Clicking the line (or tabbing to it and pressing Enter or Space) reveals everything else inside details, and the triangle points down. Clicking again hides it. There's no script, no ARIA attributes to manage and no class to toggle.
A few rules make it behave properly:
summary must be the first child of details. If it's missing, the browser invents a heading that says "Details", which is rarely what you want.summary per details. Everything after it is the hidden content, and it can be any HTML: paragraphs, lists, images, forms or even another details.open attribute controls the state. Write <details open> to show a panel when the page loads. The browser adds and removes the attribute as people click, so you can style the open state with details[open].window.find() found the word but left the panel closed.This makes details ideal for FAQs, "show more" sections, long terms and conditions, and filters on a product list. It's one of the semantic elements I recommend in my guide to HTML semantic tags, because the element itself tells browsers and assistive technology exactly what it is.
A set of details elements is already an accordion where every panel opens independently. For the classic "only one open at a time" behaviour, you used to need JavaScript. Now you just give them the same name, the same way radio buttons in a group share one.
<section class="faq">
<h2>Delivery questions</h2>
<details name="delivery" open>
<summary>How long does UK delivery take?</summary>
<div class="answer"><p>Standard delivery takes 2 to 4 working days.</p></div>
</details>
<details name="delivery">
<summary>Do you deliver to Northern Ireland?</summary>
<div class="answer"><p>Yes, at the same price as Great Britain.</p></div>
</details>
<details name="delivery">
<summary>Can I change my address after ordering?</summary>
<div class="answer"><p>Yes, until the order is dispatched.</p></div>
</details>
</section>
My test script loaded the page, clicked panels and pressed keys, and recorded the state of the three panels after each step:
Chrome: Chrome/154.0.8037.57
on load: open, closed, closed
click 2nd summary: closed, open, closed
Enter on 3rd: closed, closed, open
Space on 3rd: closed, closed, closed
toggle events: #0 closed->open | #1 closed->open | #0 open->closed | #2 closed->open | #1 open->closed | #2 open->closed
open attribute.name="delivery" grouping at work.open attribute as a change.The name attribute is supported in current Chrome, Edge, Firefox and Safari. In an older browser it's simply ignored, and the panels open independently, which is a perfectly acceptable fallback. Use a different name for each separate accordion on the page, or the panels in both groups will close each other.
You don't need JavaScript for the accordion itself, but you might want it for analytics or to save which panel was open. Every details element fires a toggle event after it opens or closes, and the event tells you both states:
<script>
window.toggleLog = [];
document.querySelectorAll("details").forEach((d, i) =>
d.addEventListener("toggle", e => window.toggleLog.push(`#${i} ${e.oldState}->${e.newState}`)));
</script>
You can also open a panel from code with element.open = true. My test did that on a separate, unstyled details element and confirmed that open was true straight afterwards. That's handy for opening the panel that matches the URL fragment, so a link like /faq#delivery can land on the right answer already expanded.
The default triangle is a marker, the same kind of thing as a bullet on a list item. My test read the computed styles of a plain summary and a styled one:
styled summary: {"display":"flex","listStyle":"none","after":"\"+\""}
plain summary: {"display":"list-item","marker":"disclosure-closed"}
The plain summary is display: list-item with a disclosure-closed marker. To remove it, set list-style: none, which works in Chrome, Edge and Firefox. Older versions of Safari draw the marker with a separate pseudo-element, so add summary::-webkit-details-marker { display: none; } too. Switching the summary to display: flex (or block) also removes the marker, because it's no longer a list item, and flexbox makes it easy to push your own icon to the right-hand edge. If you're not sure which layout to reach for, my comparison of CSS flexbox and grid covers this kind of one-row alignment.
Here's the full stylesheet from the tested accordion:
.faq details { border: 1px solid #c8d0e0; border-radius: 8px; margin-block: 8px; }
.faq summary {
cursor: pointer; padding: 12px 16px; font-weight: 600;
list-style: none; /* removes the marker in Chrome/Firefox */
display: flex; justify-content: space-between; align-items: center;
}
.faq summary::-webkit-details-marker { display: none; } /* Safari */
.faq summary::after { content: "+" / ""; font-size: 1.25rem; }
.faq details[open] > summary::after { content: "−" / ""; }
.faq summary:focus-visible { outline: 3px solid #1d70b8; outline-offset: 2px; }
.faq .answer { padding: 0 16px 12px; }
The icon is a "+" when the panel is closed and a "−" (a real minus sign) when it's open, using details[open] > summary::after. Use the child combinator >, not a space. With a space, a nested details inside an open panel would show the "−" even while it's closed, because its parent is open.
Keep cursor: pointer so the summary looks clickable, and keep a visible focus style. The :focus-visible outline in this example only appears for keyboard users, so mouse users don't see a box after clicking, but people tabbing through the page always know where they are. Don't remove the marker without adding another visual cue, or sighted users won't know the line expands.
Once the marker is sorted, most of the design work is on the open state. A few patterns I use on real sites:
details[open] { border-color: #1d70b8; } makes it obvious which answer is showing, which helps on long FAQ pages where the open panel can scroll out of view.div (the .answer class above) rather than padding details itself, so the summary keeps its full clickable area and the content lines up with the question text.summary is a block-level flex container here, the click target stretches across the full width, not just the text. That's much easier to hit on a phone.::details-content for content styles. In browsers that support it, details::details-content { padding: 0 16px; } styles the hidden part without an extra wrapper. I still keep the wrapper for older browsers.align-items: center keeps the icon vertically centred, and gap: 1rem on the flex container stops the text running into it. For one-line summaries in a narrow sidebar, the ellipsis technique from my post on CSS text-overflow ellipsis also works inside a summary, as long as you add min-width: 0 to the text element.This is the bug my test caught. Text added with ::after { content: "+" } becomes part of the summary's accessible name, the text a screen reader announces. Here's what Chrome reported, for my first version and the fixed one:
accordion.html accessible name: "How long does UK delivery take? −" ::after content: "−"
accordion-fixed.html accessible name: "How long does UK delivery take?" ::after content: "−" / ""
With the plain content: "+", a screen reader user hears "How long does UK delivery take? minus" or "plus" on every heading. The fix is CSS's alternative-text syntax: content: "+" / "". The part after the slash is the text for assistive technology, and an empty string means "say nothing". The icon still shows on screen, and the accessible name is now just the question. Browsers that don't understand the slash syntax ignore the whole declaration, so if you need to support old browsers, write the plain content: "+" line first and the "+" / "" line after it.
Chrome reported the summary's role as a disclosure triangle, which screen readers announce as a button that can be expanded or collapsed, and they read the expanded or collapsed state for you. That's the main reason I prefer details to a home-made accordion of divs and click handlers: you get the right semantics without writing a single ARIA attribute.
For years, details couldn't be animated, because the content jumped straight from hidden to visible. Two newer CSS features fix that: the ::details-content pseudo-element, which selects everything after the summary, and interpolate-size: allow-keywords, which lets the browser animate to and from auto heights.
/* Animation (progressive enhancement) */
.faq { interpolate-size: allow-keywords; }
.faq details::details-content {
block-size: 0; overflow: hidden;
transition: block-size .3s ease, content-visibility .3s allow-discrete;
}
.faq details[open]::details-content { block-size: auto; }
@media (prefers-reduced-motion: reduce) {
.faq details::details-content { transition: none; }
}
To check it really animates, my script measured the height of the second panel just before clicking, 100 milliseconds after, and 600 milliseconds after, once with normal settings and once with the reduced-motion preference switched on:
reduced-motion=no-preference closed=49px after 100ms=78px after 600ms=111px
reduced-motion=reduce closed=49px after 100ms=111px after 600ms=111px
With normal settings, the panel was part-way open at 100 ms (78 px, between the closed 49 px and the open 111 px), so the transition ran. With reduced motion, it was fully open at 100 ms, because the media query turns the transition off. Always include that query. Sliding and zooming effects can make some people feel unwell, and the setting is easy to respect.
The content-visibility .3s allow-discrete part matters too. The browser hides closed content with content-visibility, and without allow-discrete the content would vanish instantly when closing, before the height had shrunk. Browsers that don't support ::details-content or interpolate-size just open and close instantly, which is exactly how details has always worked, so this is a safe progressive enhancement. Don't animate with max-height hacks or JavaScript height measuring any more; this is shorter, and it doesn't break when the content changes size.
The element is a disclosure widget: one heading that shows or hides one block of content. That's a narrower job than it first looks, and plenty of interface patterns that look like an accordion behave differently when you use them with a keyboard or a screen reader. Before reaching for details, ask whether the hidden content is optional extra detail. If it is, details is perfect. If it isn't, one of these is probably a better fit:
role="tablist" and arrow-key navigation, not an accordion.details dropdown can work for a simple mobile menu, but it won't close when you click elsewhere on the page without some JavaScript.h3 inside summary, and it helps people who navigate by headings, but some screen readers then announce the heading role and lose the button role. Test with a real screen reader before relying on it. A plain text summary is the safe default.Also remember that the open or closed state isn't saved. If someone opens a panel and refreshes the page, it's closed again. If that matters, save the state in localStorage from the toggle event, the same way my dark mode toggle with localStorage remembers a theme.
summary. The browser shows the word "Details" as the heading.cursor: pointer.content: "+" is added to the accessible name. Use content: "+" / "".details[open] summary also matches nested closed panels. Use details[open] > summary.name for two accordions. Panels in both groups will close each other. Give each group its own name.summary. Clicking them also toggles the panel, which confuses everyone. Keep interactive elements in the content.prefers-reduced-motion. Always switch the transition off for people who ask for less motion.:focus-visible instead of hiding focus.Use several details elements, each with a summary heading, and give them the same name attribute, for example <details name="faq">. The browser then lets only one panel in the group be open at a time. Clicking, keyboard support and the open state all work without JavaScript, and older browsers fall back to independent panels.
Set summary { list-style: none; } for Chrome, Edge and Firefox, and add summary::-webkit-details-marker { display: none; } for Safari. Setting display: flex or display: block on the summary also removes it. Add your own icon with summary::after and change it with details[open] > summary::after.
Add the open attribute: <details open>. The panel shows its content when the page loads, and visitors can still close it. In JavaScript, element.open = true or false opens and closes it, and the toggle event fires afterwards.
Yes, in current browsers. Transition the block-size of details::details-content from 0 to auto, set interpolate-size: allow-keywords on a parent, and include content-visibility with allow-discrete in the transition. My test measured the panel part-way open after 100 ms. Turn the transition off inside @media (prefers-reduced-motion: reduce).
Yes. Browsers expose the summary as an expandable button and announce whether it's expanded, and it works with the keyboard out of the box. Keep the summary text short, keep a visible focus style, and hide decorative icons from screen readers with the content: "+" / "" syntax. The MDN reference for the details element lists every attribute, including name.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.
Find duplicates with GROUP BY and HAVING, preview them with ROW_NUMBER(), then delete all but one with a self-join. Error 1093, NULLs, case and spaces, and a UNIQUE key.
structuredClone(obj) makes a true deep copy that keeps Dates, Maps, Sets and circular references. When spread, Object.assign and JSON.stringify break, and how to copy classes.