Feature 320 Content

Accordion ย่อ-ขยายเนื้อหา

FAQ from SQL Server in collapsible panels, with single-open mode, expand all and search.

Live demo

Everything here talks to the real Spring Boot + SQL Server back end.

Accessibility

1Can I use the app with only a keyboard?

Yes. Every control is focusable, dialogs trap focus, tabs use arrow keys, cards move with Alt + arrows and ? lists all shortcuts.

2What about reduced motion?

If your system asks for reduced motion, animations, autoplay and smooth scrolling are switched off.

Database

1Which database does it use?

Microsoft SQL Server (Express is fine). Flyway creates the tables from db/migration/V1__schema.sql on the first start.

2Where are my theme settings saved?

In the ui_preference table: one row per browser with theme_mode, font_scale, theme_color and sidebar_collapsed.

3How do I reset the sample data?

Open Home and press 'Reset demo data'. Tasks, notes, orders, shortcuts and notifications go back to the originals.

General

1What is this project?

A study app with 25 small UI/UX lessons (features 306-330). Each lesson has a live demo and shows the real Java, SQL, HTML, CSS and JavaScript that make it work.

2Do I need an account?

No. The app gives your browser a random id in a cookie (UIUX_CLIENT) and stores your appearance settings under that id.

3Which libraries are used on the page?

None. Every component is plain HTML, CSS and JavaScript, so you can read all of it.

Privacy

1What is in the cookie?

Only a random UUID. It is HttpOnly and SameSite=Lax and contains no personal data.

2Is anything sent to other websites?

No. Fonts, icons and pictures are all served by this app. The pictures are SVG drawings generated from a number.

What to notice

  • Built on <details>/<summary>: works even without JavaScript.
  • The height animation uses the Web Animations API (element.animate).
  • Search filters questions and answers and opens the matches.

Try it

Source code

4 parts, read from the files in this project
src/main/resources/db/migration/V1__schema.sql faq table
CREATE TABLE faq (
    id         INT IDENTITY(1,1) NOT NULL CONSTRAINT pk_faq PRIMARY KEY,
    category   NVARCHAR(40)   NOT NULL,
    question   NVARCHAR(200)  NOT NULL,
    answer     NVARCHAR(1000) NOT NULL,
    sort_order INT            NOT NULL
);
src/main/resources/templates/demos/l320.html Each FAQ row from SQL Server becomes a <details> element
<div id="faq">
  <div th:each="g : ${faqGroups}" class="faq-group">
    <p class="acc-cat" th:text="${g.key}">General</p>
    <div class="accordion">
      <details class="acc-item" th:each="f, s : ${g.value}" th:open="${g.key == 'General' and s.first}">
        <summary><span class="acc-num" th:text="${s.count}">1</span><span class="acc-q" th:text="${f.question}">Q</span>
          <svg class="i chev"><use href="#i-chevron-down"/></svg></summary>
        <div class="acc-body"><p class="acc-a" th:text="${f.answer}">A</p></div>
      </details>
    </div>
  </div>
</div>
src/main/resources/static/css/app.css Accordion built on <details>
.accordion { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
.acc-item + .acc-item { border-top: 1px solid var(--border); }
.acc-item summary {
  list-style: none; display: flex; align-items: center; gap: .75rem; padding: .9rem 1rem; cursor: pointer; font-weight: 600; font-size: .92rem;
}
.acc-item summary::-webkit-details-marker { display: none; }
.acc-item summary:hover { background: var(--surface-2); }
.acc-item summary .chev { margin-left: auto; transition: transform .25s var(--ease); color: var(--text-3); }
.acc-item[open] summary .chev { transform: rotate(180deg); }
.acc-item[open] summary { color: var(--primary-text); }
.acc-body { overflow: hidden; }
.acc-body p { padding: 0 1rem 1rem 3.25rem; margin: 0; color: var(--text-2); font-size: .9rem; }
.acc-num { width: 1.6rem; height: 1.6rem; flex: none; border-radius: .45rem; display: grid; place-items: center; font-size: .72rem; background: var(--surface-2); color: var(--text-3); }
.acc-item[open] .acc-num { background: var(--primary); color: var(--on-primary); }
.acc-cat { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); margin: 1.25rem 0 .5rem; }
mark.hl { background: color-mix(in srgb, var(--warning) 35%, transparent); color: inherit; border-radius: .2rem; padding: 0 .1rem; }
src/main/resources/static/js/demos.js Animate open / close with element.animate()
function setOpen(d, open) {
  if (d.open === open || d._anim) return;
  const body = $('.acc-body', d);
  if (UI.reducedMotion()) { d.open = open; return; }
  if (open) {
    d.open = true;
    const h = body.scrollHeight;
    d._anim = body.animate([{ height: '0px', opacity: 0 }, { height: h + 'px', opacity: 1 }], { duration: 220, easing: 'ease-out' });
  } else {
    const h = body.scrollHeight;
    d._anim = body.animate([{ height: h + 'px', opacity: 1 }, { height: '0px', opacity: 0 }], { duration: 180, easing: 'ease-in' });
  }
  d._anim.onfinish = () => { d._anim = null; if (!open) d.open = false; };
}
items.forEach((d) => {
  $('summary', d).addEventListener('click', (e) => {
    e.preventDefault(); // we toggle ourselves so we can animate
    const open = !d.open;
    if (open && $('#faqSingle').checked) items.forEach((o) => o !== d && setOpen(o, false));
    setOpen(d, open);
  });
});

Are you sure?

Rename

↑↓ moveEnter openEsc close