return function () {
    $self = url('/@object/@action/' . urlencode('html/bs5_options'));
    $svg = function ($text, $color, $w = 870, $h = 300) {
        $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' . $w . '" height="' . $h . '" viewBox="0 0 ' . $w . ' ' . $h . '">'
            . '<rect width="' . $w . '" height="' . $h . '" fill="' . $color . '"/>'
            . '<text x="' . ($w / 2) . '" y="' . ($h / 2 + 12) . '" fill="#fff" font-family="sans-serif" font-size="34" text-anchor="middle">' . _prepare_html($text) . '</text></svg>';
        return 'data:image/svg+xml,' . rawurlencode($svg);
    };
    $close = fn ($text = 'Close') => html()->button($text, ['attr' => ['data-bs-dismiss' => 'modal', 'data-dismiss' => 'modal']]);
    $slides = fn ($prefix) => [
        ['img' => $svg($prefix . ' one', '#0d6efd', 600, 220), 'alt' => 'One', 'desc' => '<h5>One</h5>'],
        ['img' => $svg($prefix . ' two', '#198754', 600, 220), 'alt' => 'Two', 'desc' => '<h5>Two</h5>'],
        ['img' => $svg($prefix . ' three', '#dc3545', 600, 220), 'alt' => 'Three', 'desc' => '<h5>Three</h5>'],
    ];
    // the item keys (list_group link / active / disabled / variant, navbar sub, progress label / striped / animated, slide interval)
    // need $PROJECT_CONF['html']['B5_ITEM_OPTIONS'] = true (default off until 4.0); the docs site sets it, this page too
    _class('html')->B5_ITEM_OPTIONS = true;
    $htmx = _class('htmx');
    // a carousel that htmx swaps in: under bs5 the yf_bs5_carousel asset starts it
    if ($htmx->_wants_fragment() && ($_GET['hx'] ?? '') === 'carousel') {
        return $htmx->_result('<div id="demo-hx-carousel" style="max-width:600px">' . html()->carousel($slides('Swapped'), ['interval' => 1000, 'id' => 'demo-carousel-hx']) . '</div>');
    }
    asset('htmx');

    $intro = '<p class="docs-demo-intro">Options of the existing <code>html()</code> widgets (<code>yf_html</code>, Bootstrap 5 markup in
        <code>yf_html_bs5</code>). Switch the framework with <a href="' . $self . '?bs=5">?bs=5</a> / <a href="' . $self . '?bs=3">?bs=3</a>.
        Every option is active only when it is passed: a call without them renders exactly as before. The item keys (list group
        <code>link</code> / <code>active</code> / <code>disabled</code> / <code>variant</code>, navbar <code>sub</code>, progress
        <code>label</code> / <code>striped</code> / <code>animated</code>, a slide <code>interval</code>) also need
        <code>$PROJECT_CONF[\'html\'][\'B5_ITEM_OPTIONS\'] = true</code> (default off until 4.0), so items built from database rows do
        not change. Bootstrap 3 fallbacks are in brackets. Text options (the item names, <code>subtitle</code>, progress <code>label</code>, <code>offcanvas_title</code>) are escaped;
        bodies (tab content, card <code>body</code> / <code>footer</code>, list items, modal header / body / footer) are developer-supplied HTML and are not.</p>
        <ul class="docs-demo-intro">
        <li><code>navbar($items, [\'collapse\' => \'md\', \'dark\' => 1, \'items_right\' => [...], \'offcanvas\' => true|\'start\', \'offcanvas_title\', \'theme\'])</code>;
            an item with <code>\'sub\' => [...]</code> (<code>dropdown()</code> items) is a dropdown, <code>items_right</code> items are active with <code>active</code>.
            (<code>navbar-toggle</code> + collapse at 768px, <code>navbar-inverse</code>, <code>navbar-right</code>; offcanvas is a collapse.)</li>
        <li><code>modal([\'size\' => \'sm|lg|xl\', \'fullscreen\' => true|\'md\', \'centered\', \'scrollable\', \'static\', \'fade\', \'theme\'])</code> and
            <code>modal_button($text, $id, $button_options)</code>. (sm / lg, xl is lg; static, fade; centered, scrollable and fullscreen are ignored.)</li>
        <li><code>tabs($tabs, [\'type\' => \'tabs|pills|underline\', \'fill\', \'justified\', \'vertical\'])</code>, also form2
            <code>form($r, [\'tabs\' => [\'type\' => \'pills\']])</code>. (underline is tabs, fill is <code>nav-justified</code>, vertical is <code>nav-stacked</code> in a column.)</li>
        <li><code>list_group($items, [\'numbered\', \'flush\', \'theme\'])</code>, items <code>[\'body\', \'link\', \'active\', \'disabled\', \'variant\' => \'success\']</code>;
            with a <code>link</code> the group is a <code>div</code> of links (<code>a.list-group-item</code>; numbered is an <code>ol</code> without numbers, flush is ignored).
            <code>link</code> options, navbar <code>items_right</code> / <code>sub</code> links and the card <code>img_top</code> take trusted URLs;
            <code>javascript:</code>, <code>data:</code>, <code>vbscript:</code> become <code>#</code>.</li>
        <li><code>card([...])</code> = <code>panel()</code> with <code>\'subtitle\'</code>, <code>\'footer\'</code>, <code>\'img_top\'</code> / <code>\'img_alt\'</code>,
            <code>\'border\' => \'primary\'</code>, <code>\'theme\'</code>. (<code>panel-footer</code>, the image under the heading, <code>panel-primary</code>, subtitle as <code>small</code>.)</li>
        <li><code>progress_bar($bars, [\'striped\', \'animated\', \'height\' => 24])</code>, per bar <code>\'label\'</code>, <code>\'striped\'</code>, <code>\'animated\'</code>.
            (<code>progress-bar-striped active</code>.)</li>
        <li><code>accordion($items, [\'flush\', \'always_open\', \'theme\'])</code>. (always open: no <code>data-parent</code>; flush is ignored.)</li>
        <li><code>carousel($slides, [\'fade\', \'interval\' => 5000|false, \'pause\' => \'hover\'|false, \'ride\', \'touch\', \'wrap\', \'keyboard\', \'theme\'])</code>,
            per slide <code>\'interval\'</code>; the jQuery-free <code>yf_bs5_carousel</code> asset starts carousels htmx swaps in. (interval, pause, wrap; no fade.)</li>
        <li>Colour modes: <code>$CONF[\'bs_color_mode\'] = \'light|dark|auto\'</code> (or <code>bs_color_mode_user</code> / <code>_admin</code>) puts
            <code>data-bs-theme</code> on <code><html></code>; with <code>bs_theme_allow_override_for_user</code> the header gets a light / dark / auto
            switcher next to the theme switcher (asset <code>yf_bs5_color_modes</code>, remembered in a cookie). Per widget: <code>\'theme\' => \'dark\'</code>.
            (Bootstrap 3: ignored.)</li>
        </ul>';

    $nav_items = [
        'brand' => ['name' => 'Brand', 'link' => $self],
        'home' => ['name' => 'Home', 'link' => $self],
        'docs' => ['name' => 'Docs', 'link' => $self . '?p=docs'],
        'more' => ['name' => 'More', 'sub' => [
            $self . '?p=a' => 'Action',
            ['name' => 'Another action', 'link' => $self . '?p=b'],
            '-',
            ['header' => 'Section'],
            ['text' => 'Disabled', 'link' => $self . '?p=c', 'disabled' => 1],
        ]],
    ];
    $navbars = '<div id="demo-navbar-wrap">' . html()->navbar($nav_items, [
            'id' => 'demo-navbar',
            'collapse' => 'md',
            'dark' => 1,
            'items_right' => [
                ['name' => 'Sign in', 'link' => $self . '?p=login', 'active' => 1],
                ['name' => 'Account', 'sub' => [$self . '?p=profile' => 'Profile', $self . '?p=logout' => 'Sign out']],
            ],
        ]) . '</div>'
        . '<p></p>' . html()->navbar(['brand' => ['name' => 'Offcanvas', 'link' => $self], ['name' => 'Home', 'link' => $self], ['name' => 'Docs', 'link' => $self . '?p=docs']], [
            'id' => 'demo-navbar-oc',
            'offcanvas' => true,
            'offcanvas_title' => 'Menu',
        ]);

    $long = str_repeat('<p>A long body: the dialog scrolls inside, the page does not.</p>', 30);
    $modals = '';
    $buttons = [];
    foreach ([
        'sm' => ['Small', ['size' => 'sm']],
        'lg' => ['Large', ['size' => 'lg']],
        'xl' => ['Extra large', ['size' => 'xl']],
        'fs' => ['Fullscreen', ['fullscreen' => true]],
        'centered' => ['Centered, scrollable', ['centered' => 1, 'scrollable' => 1, 'body' => $long]],
        'static' => ['Static backdrop, fade', ['static' => 1, 'fade' => 1, 'body' => '<p>A click outside or Escape does not close it; the button does.</p>']],
        'dark' => ['Dark', ['theme' => 'dark', 'fade' => 1]],
    ] as $key => [$title, $opts]) {
        $modals .= html()->modal($opts + [
            'id' => 'demo-modal-' . $key,
            'header' => $title,
            'show_close' => 1,
            'body' => '<p>' . $title . ' modal.</p>',
            'footer' => $close(),
        ]);
        $buttons[] = html()->modal_button($title, 'demo-modal-' . $key, ['type' => 'primary', 'outline' => 1, 'id' => 'demo-modal-' . $key . '-btn']);
    }
    $modals = '<p id="demo-modal-buttons">' . implode(' ', $buttons) . '</p>' . $modals;

    $tab_items = ['home' => '<p>Home pane.</p>', 'profile' => '<p>Profile pane.</p>', 'contact' => '<p>Contact pane.</p>'];
    $tabs = '<div id="demo-tabs-pills">' . html()->tabs($tab_items, ['type' => 'pills', 'id' => 'demo-pills', 'links_prefix' => 'pills_']) . '</div>'
        . '<div id="demo-tabs-underline" class="mt-3">' . html()->tabs($tab_items, ['type' => 'underline', 'fill' => 1, 'id' => 'demo-underline', 'links_prefix' => 'under_']) . '</div>'
        . '<div id="demo-tabs-vertical" class="mt-3">' . html()->tabs($tab_items, ['type' => 'pills', 'vertical' => 1, 'id' => 'demo-vertical', 'links_prefix' => 'vert_']) . '</div>';

    $lists = '<div class="row"><div class="col-sm-4">' . html()->list_group([
            ['body' => 'Active link', 'link' => $self . '?p=1', 'active' => 1],
            ['body' => 'A link', 'link' => $self . '?p=2', 'badge' => 4],
            ['body' => 'Disabled link', 'link' => $self . '?p=3', 'disabled' => 1],
            ['body' => 'Success', 'link' => $self . '?p=4', 'variant' => 'success'],
        ], ['id' => 'demo-lg-links']) . '</div>'
        . '<div class="col-sm-4">' . html()->list_group(['First', 'Second', ['body' => 'Third', 'variant' => 'warning']], ['numbered' => 1, 'id' => 'demo-lg-numbered']) . '</div>'
        . '<div class="col-sm-4">' . html()->list_group(['Flush one', 'Flush two', 'Dark theme'], ['flush' => 1, 'theme' => 'dark', 'id' => 'demo-lg-flush']) . '</div></div>';

    $cards = '<div class="row"><div class="col-sm-6">' . html()->card([
            'id' => 'demo-card',
            'title' => 'Card title',
            'subtitle' => 'Subtitle <escaped>',
            // img_top is scheme-checked like a link: a data: URI becomes #
            'img_top' => url('/dynamic/placeholder/600x200'),
            'img_alt' => 'A grey placeholder',
            'body' => '<p>Body text of the card.</p>',
            'footer' => 'Footer',
            'border' => 'primary',
        ]) . '</div><div class="col-sm-6">' . html()->card(['id' => 'demo-card-dark', 'title' => 'Dark card', 'body' => '<p>theme => dark</p>', 'footer' => 'Footer', 'theme' => 'dark']) . '</div></div>';

    $progress = html()->progress_bar([['val' => 60, 'label' => '60%', 'type' => 'success']], ['striped' => 1, 'animated' => 1, 'height' => 24, 'id' => 'demo-progress'])
        . '<p></p>' . html()->progress_bar([['val' => 30, 'label' => 'Done 30%'], ['val' => 25, 'label' => '25%', 'type' => 'warning', 'striped' => 1], ['val' => 15, 'type' => 'danger']], ['id' => 'demo-progress-stacked']);

    $accordion = html()->accordion([
        'first' => '<p>Open on load. Opening another one keeps this one open (always_open).</p>',
        'second' => '<p>Second body.</p>',
        'third' => '<p>Third body.</p>',
    ], ['flush' => 1, 'always_open' => 1, 'id' => 'demo-accordion']);

    $carousel = '<div style="max-width:600px">' . html()->carousel($slides('Slide'), ['fade' => 1, 'interval' => 1500, 'pause' => false, 'id' => 'demo-carousel']) . '</div>'
        . '<p class="mt-3">' . html()->button('Load a carousel through htmx', ['type' => 'primary', 'outline' => 1, 'id' => 'demo-hx-carousel-btn', 'attr' => [
            'hx-get' => $self . '?hx=carousel',
            'hx-target' => '#demo-hx-carousel',
            'hx-swap' => 'outerHTML',
        ]]) . '</p><div id="demo-hx-carousel"></div>';

    $mode = common()->bs_color_mode();
    $modes = '<p id="demo-color-mode">Colour mode of this page: <b>' . ($mode !== '' ? _prepare_html($mode) : 'off (Bootstrap 3, or no bs_color_mode)') . '</b>.'
        . ' Use the switcher in the header (Bootstrap 5); the dark card, the dark list group and the dark modal above keep their own mode.</p>';

    return $intro
        . '<h3>Navbar: collapse, dark, dropdowns, items on the right, offcanvas</h3>' . $navbars
        . '<h3>Modal sizes and options</h3>' . $modals
        . '<h3>Tabs: pills, underline + fill, vertical</h3>' . $tabs
        . '<h3>List groups: links, numbered, flush</h3>' . $lists
        . '<h3>Cards</h3>' . $cards
        . '<h3>Progress</h3>' . $progress
        . '<h3>Accordion: flush, always open</h3>' . $accordion
        . '<h3>Carousel: fade, 1.5 s interval, no pause; one loaded through htmx</h3>' . $carousel
        . '<h3>Colour modes</h3>' . $modes;
};

Options of the existing html() widgets (yf_html, Bootstrap 5 markup in yf_html_bs5). Switch the framework with ?bs=5 / ?bs=3. Every option is active only when it is passed: a call without them renders exactly as before. The item keys (list group link / active / disabled / variant, navbar sub, progress label / striped / animated, a slide interval) also need $PROJECT_CONF['html']['B5_ITEM_OPTIONS'] = true (default off until 4.0), so items built from database rows do not change. Bootstrap 3 fallbacks are in brackets. Text options (the item names, subtitle, progress label, offcanvas_title) are escaped; bodies (tab content, card body / footer, list items, modal header / body / footer) are developer-supplied HTML and are not.

  • navbar($items, ['collapse' => 'md', 'dark' => 1, 'items_right' => [...], 'offcanvas' => true|'start', 'offcanvas_title', 'theme']); an item with 'sub' => [...] (dropdown() items) is a dropdown, items_right items are active with active. (navbar-toggle + collapse at 768px, navbar-inverse, navbar-right; offcanvas is a collapse.)
  • modal(['size' => 'sm|lg|xl', 'fullscreen' => true|'md', 'centered', 'scrollable', 'static', 'fade', 'theme']) and modal_button($text, $id, $button_options). (sm / lg, xl is lg; static, fade; centered, scrollable and fullscreen are ignored.)
  • tabs($tabs, ['type' => 'tabs|pills|underline', 'fill', 'justified', 'vertical']), also form2 form($r, ['tabs' => ['type' => 'pills']]). (underline is tabs, fill is nav-justified, vertical is nav-stacked in a column.)
  • list_group($items, ['numbered', 'flush', 'theme']), items ['body', 'link', 'active', 'disabled', 'variant' => 'success']; with a link the group is a div of links (a.list-group-item; numbered is an ol without numbers, flush is ignored). link options, navbar items_right / sub links and the card img_top take trusted URLs; javascript:, data:, vbscript: become #.
  • card([...]) = panel() with 'subtitle', 'footer', 'img_top' / 'img_alt', 'border' => 'primary', 'theme'. (panel-footer, the image under the heading, panel-primary, subtitle as small.)
  • progress_bar($bars, ['striped', 'animated', 'height' => 24]), per bar 'label', 'striped', 'animated'. (progress-bar-striped active.)
  • accordion($items, ['flush', 'always_open', 'theme']). (always open: no data-parent; flush is ignored.)
  • carousel($slides, ['fade', 'interval' => 5000|false, 'pause' => 'hover'|false, 'ride', 'touch', 'wrap', 'keyboard', 'theme']), per slide 'interval'; the jQuery-free yf_bs5_carousel asset starts carousels htmx swaps in. (interval, pause, wrap; no fade.)
  • Colour modes: $CONF['bs_color_mode'] = 'light|dark|auto' (or bs_color_mode_user / _admin) puts data-bs-theme on <html>; with bs_theme_allow_override_for_user the header gets a light / dark / auto switcher next to the theme switcher (asset yf_bs5_color_modes, remembered in a cookie). Per widget: 'theme' => 'dark'. (Bootstrap 3: ignored.)

Navbar: collapse, dark, dropdowns, items on the right, offcanvas

Modal sizes and options

Tabs: pills, underline + fill, vertical

Home pane.

Profile pane.

Contact pane.

Home pane.

Profile pane.

Contact pane.

Home pane.

Profile pane.

Contact pane.

List groups: links, numbered, flush

  1. First
  2. Second
  3. Third
  • Flush one
  • Flush two
  • Dark theme

Cards

Card title Subtitle <escaped>

A grey placeholder

Body text of the card.

Dark card

theme => dark

Progress

60%

Done 30%
25%

Accordion: flush, always open

Open on load. Opening another one keeps this one open (always_open).

Second body.

Third body.

Carousel: fade, 1.5 s interval, no pause; one loaded through htmx

Colour modes

Colour mode of this page: off (Bootstrap 3, or no bs_color_mode). Use the switcher in the header (Bootstrap 5); the dark card, the dark list group and the dark modal above keep their own mode.