Skip to content

Example Multi Channel Accordion

Michael Dörer edited this page Oct 12, 2026 · 1 revision

Example: Multi-Channel Accordion

The accordion component (livingdocs/100.Misc/020.accordion/) shows how to give editors one structure in the editor that ends up as completely different markup in each output channel:

Channel Result
Editor Accordion look: header bar with heading, icon and content area below
Web app Interactive, accessible accordion built by Vue (button, aria-expanded, keyboard and focus handling, deep links)
PDF, Word, XHTML No accordion at all: the heading and the content follow each other as if the editor had placed them without a wrapper. The content is always visible.

The tool for this is the data-remove-from-<target> attribute on almost every element of the template (see Component Library#output-specific-markup).

flowchart LR
  E["Editor template<br/>root + 5 wrapper divs + icon<br/>+ template-header + template-content"]
  E -->|web export| W["srl-ld-accordion<br/>+ template-header + template-content"]
  W -->|"prepareHtmlContent + accordion.vue"| A["Vue accordion<br/>h2 > button, hidden content"]
  E -->|pdf / word / xhtml export| P["h2 + content<br/>(no wrappers)"]
Loading

Files

File Purpose
accordion.html Editor template with the per-channel attributes
ld-conf.json Two containers: header (only title-h2 … title-h6, default title-h2) and content (default paragraph); property pdf-spacer
accordion.vue Web app rendering of <srl-ld-accordion>
scss/editor.scss Makes the custom elements template-header and template-content behave like blocks in the editor

The app uses the SRL runtime components SrlCategoryAccordion, SrlCategoryAccordionToggle and SrlCategoryAccordionContent (.srl/components/Srl/Category/). The visual styles of .srl-category-accordion… live in the project's global SCSS (in the library's dev project: dev/src/assets/scss/components/accordion.scss).

The template

<srl-ld-accordion class="srl-category-accordion srl-category-accordion--category"
  data-remove-from-pdf="transient"
  data-remove-from-word="transient"
  data-remove-from-xhtml="transient"
>
  <div class="srl-category-accordion__inner"
    data-remove-from-web="transient" data-remove-from-pdf="transient"
    data-remove-from-word="transient" data-remove-from-xhtml="transient">
    <div class="srl-category-accordion__inner-head" …all four transient…>
      <div class="srl-category-accordion__head" …all four transient…>
        <div class="srl-category-accordion__head--btn" …all four transient…>
          <template-header doc-container="header"
            data-remove-from-pdf="transient"
            data-remove-from-word="transient"
            data-remove-from-xhtml="transient"
          ></template-header>
          <span class="srl-category-accordion__icon"
            data-remove-from-web="complete" data-remove-from-pdf="complete"
            data-remove-from-word="complete" data-remove-from-xhtml="complete"
          ></span>
        </div>
      </div>
    </div>
    <div class="srl-category-accordion__content" …all four transient…>
      <template-content class="srl-category-accordion__content-inner" doc-container="content"
        data-remove-from-pdf="transient"
        data-remove-from-word="transient"
        data-remove-from-xhtml="transient"
      ></template-content>
    </div>
  </div>
</srl-ld-accordion>

What stays where

Element Editor Web PDF Word XHTML
<srl-ld-accordion> (root) ✅ ✅ → accordion.vue transient transient transient
__inner, __inner-head, __head, __head--btn, __content ✅ transient transient transient transient
<template-header> ✅ container ✅ → slot #header transient transient transient
<template-content> ✅ container ✅ → slot #content transient transient transient
__icon ✅ complete complete complete complete
Content of the containers (heading, paragraphs …) ✅ ✅ ✅ ✅ ✅

Read the table in three layers:

  1. Editor-only scaffolding. The five wrapper divs and the icon exist only so that the accordion looks like an accordion in the editor. They carry the same BEM classes as the Vue output, so the same .srl-category-accordion… styles apply in the editor and in the app.
  2. The web skeleton. The web export keeps exactly what accordion.vue needs: the custom element and the two template-<name> wrappers that become named slots.
  3. The content. The components inside the containers are never touched. In PDF, Word and XHTML they are all that is left.

The icon is empty, so complete is safe. Every element that contains a doc-container or doc-editable must be transient, otherwise the editor content disappears with it.

Example

An editor places an accordion and keeps the default title-h2 in the header (number 1, text Risks). In the content they add two paragraphs.

Web export

Simplified HTML that the app receives (Livingdocs attributes omitted):

<srl-ld-accordion class="srl-category-accordion srl-category-accordion--category">
  <template-header>
    <h2 class="srl-grid srl-title-h2 srl-margin-group-title-h2 srl-linkable">
      <span class="srl-grid__inner srl-title-h2__number-text-container">
        <span class="srl-title-h2__number">1</span>
        <span class="srl-title-h2__text">Risks</span>
      </span>
    </h2>
  </template-header>
  <template-content class="srl-category-accordion__content-inner">
    <p class="srl-paragraph">First paragraph …</p>
    <p class="srl-paragraph">Second paragraph …</p>
  </template-content>
</srl-ld-accordion>

prepareHtmlContent() turns <template-header> into <template #header> and <template-content> into <template class="…" #content> (see Web App Rendering#from-html-to-vue). <srl-ld-accordion> resolves to accordion.vue.

What accordion.vue does

  1. Detects the heading. The first node of the header slot must be h1 … h6. The heading number sets level = n - 2 (h2 → 0, h3 → 1 …). The root gets the modifier srl-category-accordion--level-<level>, and the toggle renders h<level + 2> again, so the document outline stays intact.
  2. Rebuilds the heading. Inside a …__number-text-container the number span gets the class srl-category-accordion__head-number and the text span srl-category-accordion__head-text. Without that container the first child of the heading becomes the text. All other classes of the original heading (srl-grid, srl-margin-group-…, srl-linkable, the h2-as styling classes) are dropped.
  3. Wraps it in the runtime components. SrlCategoryAccordion holds the state, SrlCategoryAccordionToggle renders heading + button, SrlCategoryAccordionContent renders the collapsible area.
  4. Falls back. If the header slot does not start with a heading, the component renders the header and content slots one after the other, without accordion.

Rendered DOM in the app

<div tabindex="-1" class="srl-category-accordion srl-category-accordion--category srl-category-accordion--level-0">
  <div class="srl-category-accordion__inner">
    <div class="srl-category-accordion__inner-head">
      <h2 class="srl-title-h2 srl-category-accordion__head">
        <button type="button" class="srl-category-accordion__head--btn"
                aria-expanded="false" aria-controls="v-0">
          <span class="srl-category-accordion__head-number">1</span>
          <span class="srl-category-accordion__head-text">Risks</span>
          <span class="srl-category-accordion__icon"></span>
        </button>
      </h2>
    </div>
    <div id="v-0" hidden tabindex="-1" class="srl-category-accordion__content">
      <div class="srl-category-accordion__content-inner">
        <p class="srl-paragraph">First paragraph …</p>
        <p class="srl-paragraph">Second paragraph …</p>
      </div>
    </div>
  </div>
</div>

Behaviour of the runtime components:

  • The button toggles the content (hidden, aria-expanded). Esc inside the content closes it and puts the focus back on the button.
  • Focusable elements inside a closed accordion get tabindex="-1", so keyboard users do not tab into hidden content.
  • Deep links: if the URL hash matches the id of the accordion or of an element inside it, the accordion opens on load. For a target inside the accordion, the first such accordion of the page is also scrolled into view.

PDF, Word and XHTML

Everything but the content is gone:

<h2 class="srl-grid srl-title-h2 srl-margin-group-title-h2 srl-linkable">
  <span class="srl-grid__inner srl-title-h2__number-text-container">
    <span class="srl-title-h2__number">1</span>
    <span class="srl-title-h2__text">Risks</span>
  </span>
</h2>
<p class="srl-paragraph">First paragraph …</p>
<p class="srl-paragraph">Second paragraph …</p>

This is the same markup as a title-h2 and two paragraphs placed directly in the article. The PDF uses the normal title and paragraph styles and prints the content in full. Because the root is removed from the XHTML, <srl-ld-accordion> needs no data-replace-tag.

The template has no data-remove-from-translate-plus, so the translation export keeps the complete structure.

Pitfalls

  • Properties on the root do not reach PDF, Word or XHTML. Livingdocs writes property classes onto the component root, and the root is removed transiently there. Check in the PDF output whether a class like pdf-spacer arrives before you rely on it.
  • Heading styling options are lost in the app. accordion.vue replaces the heading and keeps only the number and text spans, so h2-as and similar classes only affect the editor and the print channels.
  • Only one heading in the header. accordion.vue reads only the first node of the header slot. allowedChildren in ld-conf.json restricts the container to titles; keep it that way.
  • Whitespace. The Vue component expects the heading as the first slot node and the number/text spans as element children. This works because the design build and Vue drop whitespace between tags (see Components#templates).

Pattern for your own components

  1. Build the editor markup for the editor's look. Mark wrappers that only serve the editor transient for all channels.
  2. In the web export keep only the custom element (<srl-ld-…>) and the template-<name> wrappers that the Vue component needs as slots.
  3. Remove decorative elements without content (icons, dividers) with complete.
  4. Never use complete on an element that contains a doc-container or doc-editable.
  5. Let channels without interaction fall back to the plain content: remove the root and all wrappers transient from PDF, Word and XHTML.
  6. Use the same BEM classes in the editor markup and the Vue output, so one stylesheet serves both.
  7. Give the Vue component a fallback for content it does not expect.

Clone this wiki locally