return function () {
    $self = url('/@object/@action/' . urlencode('table/bs5_components'));
    $data = json_decode(file_get_contents(__DIR__ . '/products.json'), true);
    // The active flags live in the visitor's session (the sample rows start with a few inactive ones), never in a table
    $key = 'docs_demo_bs5_components_active';
    if ( ! isset($_SESSION[$key]) || ! is_array($_SESSION[$key])) {
        $_SESSION[$key] = [];
        foreach ($data as $id => $row) {
            $_SESSION[$key][(int) $id] = (int) $row['viewed'] >= 1000 ? 1 : 0;
        }
    }
    $toggle = (int) ($_GET['toggle'] ?? 0);
    if ($toggle && isset($_SESSION[$key][$toggle])) {
        $_SESSION[$key][$toggle] = (int) ! $_SESSION[$key][$toggle];
        // the answer of the framework's active handlers: "1" / "0" for ajax, a redirect otherwise
        if (is_ajax()) {
            no_graphics(true);
            echo $_SESSION[$key][$toggle];
            exit;
        }
        return js_redirect($self);
    }
    foreach ($data as $id => $row) {
        $data[$id]['active'] = $_SESSION[$key][(int) $id] ?? 0;
    }
    $actions = ['edit', 'view', 'clone', 'delete'];
    $act = in_array($_GET['act'] ?? '', $actions, true) ? $_GET['act'] : '';
    $notice = $act ? '<div class="alert alert-info">Row button <b>' . $act . '</b> of row <b>' . (int) ($_GET['row'] ?? 0) . '</b> was clicked (the demo only shows it).</div>' : '';

    $intro = '<p class="docs-demo-intro">Bootstrap 5 table options of <code>table()</code> (<code>yf_table2</code>) and
        <code>html()->pagination()</code>, on the sample products. Switch the framework with <a href="' . $self . '?bs=5">?bs=5</a> /
        <a href="' . $self . '?bs=3">?bs=3</a>: every option is a pure addition and falls back to Bootstrap 3 markup, and a table without
        them renders exactly as before.</p>
        <ul class="docs-demo-intro">
        <li><code>responsive</code> => <code>true</code> or a breakpoint (<code>sm</code>, <code>md</code>, <code>lg</code>, <code>xl</code>,
            <code>xxl</code>): a <code>table-responsive[-md]</code> wrapper (Bootstrap 3 has only <code>table-responsive</code>).</li>
        <li><code>sticky_header</code> => <code>true</code> (sticks while the page scrolls) or a height such as <code>\'300px\'</code> (a scroll box
            the header sticks in, also needed inside <code>responsive</code>): <code>thead.sticky-top</code> under bs5, inline
            <code>position: sticky</code> under Bootstrap 3.</li>
        <li><code>tr_variant</code> => <code>fn($row, $id)</code> returning <code>success</code>, <code>danger</code>, <code>warning</code>,
            <code>info</code>, <code>active</code>...: <code>table-success</code> under bs5, <code>success</code> under Bootstrap 3, merged with the
            <code>tr</code> option.</li>
        <li><code>buttons_group</code> => <code>true</code> and <code>buttons_dropdown_after</code> => <code>N</code>: the row buttons in one
            <code>btn-group</code>, and all after the first N in a "⋯" menu; links, classes (<code>ajax_delete</code>), rights checks stay.</li>
        <li><code>btn_active(\'Active\', $link, [\'renderer\' => \'switch\'])</code>: a <code>form-switch</code> under bs5 that posts to the
            same active URL (asset <code>yf_active_switch</code>); Bootstrap 3 keeps the buttons.</li>
        <li><code>html()->pagination($pages, [\'current\', \'disabled\', \'size\' => \'sm|lg\', \'align\' => \'center|end\'])</code> and the
            table options <code>pager_size</code> / <code>pager_align</code>.</li>
        <li>Table variants need no new option: <code>table_class_add</code>, <code>thead</code> / <code>tbody</code> classes.</li>
        </ul>';

    // sold: many (success), few (warning), almost none (danger); an inactive row otherwise gets the `active` (grey) variant
    $variant = function ($row) {
        $sold = (int) $row['sold'];
        if ($sold >= 900 || $sold < 300) {
            return $sold >= 900 ? 'success' : ($sold < 150 ? 'danger' : 'warning');
        }
        return $row['active'] ? '' : 'active';
    };

    $wide = table($data, [
            'no_pages' => 1,
            'responsive' => true,
            'sticky_header' => '300px',
            'tr_variant' => $variant,
            'table_attr' => 'id="demo-wide" style="min-width: 1400px"',
        ])
        ->text('id')
        ->text('name')
        ->text('articul')
        ->text('price')
        ->text('old_price')
        ->text('quantity')
        ->text('sold')
        ->text('viewed')
        ->date('add_date', 'Added')
        ->date('update_date', 'Updated')
        ->text('url', 'URL');

    $buttons = table(array_slice($data, 0, 6, true), [
            'no_pages' => 1,
            'buttons_group' => true,
            'buttons_dropdown_after' => 3,
            'tr_variant' => $variant,
            'table_attr' => 'id="demo-buttons"',
        ])
        ->text('id')
        ->text('name')
        ->text('sold')
        ->btn_active('Active', $self . '?toggle=%d', ['renderer' => 'switch'])
        ->btn_edit(['link' => $self . '?act=edit&row=%d', 'no_ajax' => 1])
        ->btn_view(['link' => $self . '?act=view&row=%d', 'no_ajax' => 1])
        ->btn_clone(['link' => $self . '?act=clone&row=%d', 'no_ajax' => 1])
        ->btn_delete(['link' => $self . '?act=delete&row=%d']);

    $sticky = table($data, [
            'no_pages' => 1,
            'sticky_header' => true,
            'condensed' => 1,
            'table_attr' => 'id="demo-sticky"',
        ])
        ->text('id')
        ->text('name')
        ->text('price')
        ->text('sold');

    $variants = table(array_slice($data, 0, 4, true), [
            'no_pages' => 1,
            'caption' => 'table-striped-columns, caption-top, a table-dark head and a table-group-divider body',
            'table_class_add' => 'table-striped-columns caption-top',
            'thead' => ['class' => 'table-dark'],
            'tbody' => ['class' => 'table-group-divider'],
        ])
        ->text('id')
        ->text('name')
        ->text('price');
    $dark = table(array_slice($data, 4, 3, true), ['no_pages' => 1, 'table_class_add' => 'table-dark'])
        ->text('id')
        ->text('name')
        ->text('price');

    $pages = ['prev' => '', 1 => $self . '?p=1', 2 => $self . '?p=2', 3 => $self . '?p=3', 4 => $self . '?p=4', 'next' => $self . '?p=3'];
    $pagination = '';
    foreach (['sm' => ['align' => ''], '' => ['align' => 'center'], 'lg' => ['align' => 'end']] as $size => $opts) {
        $pagination .= '<p class="text-muted">size <code>' . ($size ?: 'default') . '</code>, align <code>' . ($opts['align'] ?: 'start') . '</code>,
            current 2, prev disabled</p>'
            . html()->pagination($pages, ['current' => 2, 'disabled' => ['prev'], 'size' => $size, 'align' => $opts['align'], 'id' => 'demo-pagination-' . ($size ?: 'md')]);
    }
    $paged = table($data, [
            'pager_records_on_page' => 5,
            'pager_size' => 'sm',
            'pager_align' => 'center',
            'extra' => ['url_path' => $self . '?'],
            'table_attr' => 'id="demo-paged"',
        ])
        ->text('id')
        ->text('name')
        ->text('price');

    return $intro . $notice
        . '<h3>responsive, sticky_header (scroll box), tr_variant</h3>' . $wide
        . '<h3>buttons_group, buttons_dropdown_after, active switch</h3>' . $buttons
        . '<h3>sticky_header => true (page scroll)</h3>' . $sticky
        . '<h3>Table variants (table_class_add, thead, tbody)</h3>' . $variants . $dark
        . '<h3>html()->pagination() states and sizes</h3>' . $pagination
        . '<h3>pager_size, pager_align</h3>' . $paged;
};

Bootstrap 5 table options of table() (yf_table2) and html()->pagination(), on the sample products. Switch the framework with ?bs=5 / ?bs=3: every option is a pure addition and falls back to Bootstrap 3 markup, and a table without them renders exactly as before.

  • responsive => true or a breakpoint (sm, md, lg, xl, xxl): a table-responsive[-md] wrapper (Bootstrap 3 has only table-responsive).
  • sticky_header => true (sticks while the page scrolls) or a height such as '300px' (a scroll box the header sticks in, also needed inside responsive): thead.sticky-top under bs5, inline position: sticky under Bootstrap 3.
  • tr_variant => fn($row, $id) returning success, danger, warning, info, active...: table-success under bs5, success under Bootstrap 3, merged with the tr option.
  • buttons_group => true and buttons_dropdown_after => N: the row buttons in one btn-group, and all after the first N in a "⋯" menu; links, classes (ajax_delete), rights checks stay.
  • btn_active('Active', $link, ['renderer' => 'switch']): a form-switch under bs5 that posts to the same active URL (asset yf_active_switch); Bootstrap 3 keeps the buttons.
  • html()->pagination($pages, ['current', 'disabled', 'size' => 'sm|lg', 'align' => 'center|end']) and the table options pager_size / pager_align.
  • Table variants need no new option: table_class_add, thead / tbody classes.
Row button view of row 10058 was clicked (the demo only shows it).

responsive, sticky_header (scroll box), tr_variant

Id Name Articul Price Old price Quantity Sold Viewed Added Updated URL
10051 Archives T-shirt, black 10051 37.00 45.69 0 270 7293 09/27/13 10/04/13 archives_tee_black
10052 Archives T-shirt, navy 10052 37.00 47.67 0 719 5074 09/30/13 10/03/13 archives_tee_navy
10053 Archives T-shirt, purple 10053 37.00 47.65 0 993 218 10/02/13 10/01/13 archives_tee_purple
10056 Attacker T-shirt, white 10056 37.00 44.84 0 412 5533 09/29/13 10/04/13 attacker_tee_white
10058 Blamo T-shirt, black 10058 37.00 50.34 0 208 1796 10/01/13 09/30/13 blamo_tee_black
10059 Blamo T-shirt, white 10059 37.00 47.55 0 518 9193 10/03/13 10/02/13 blamo_tee_white
10060 Blamo T-shirt, black and green 10060 37.00 51.59 0 861 929 09/27/13 10/04/13 blamo_tee_black_green
10061 Paint Bucket T-shirt, green 10061 37.00 52.38 0 1009 3390 09/28/13 10/03/13 paint_bucket_tee_green
10062 Paint Bucket T-shirt, white 10062 37.00 53.50 0 559 2041 09/30/13 10/04/13 paint_bucket_tee_white
10063 Paint Bucket T-shirt, purple 10063 37.00 47.45 0 521 4388 09/28/13 09/30/13 paint_bucket_tee_purple
10066 Flash T-shirt, white 10066 35.00 45.73 0 90 194 09/27/13 10/04/13 flash_tee_white
10067 Mix Tape T-shirt, black 10067 45.00 60.95 0 826 6146 09/29/13 10/04/13 mix_tape_tee_black
10245 Winner T-shirt, grey 10245 35.00 0.00 0 787 4925 10/02/13 10/04/13 winner_tee_grey
10247 Hill T-shirt, white 10247 35.00 0.00 0 518 3585 10/01/13 10/04/13 hill_tee_white
10865 Roseheart T-shirt, black 10865 35.00 0.00 0 0 0 roseheart_tee_black
10866 Liberty T-shirt, black 10866 35.00 0.00 0 0 0 liberty_tee_black
10868 Pray Skull T-shirt, black 10868 35.00 0.00 0 0 0 pray_skull_tee_black
10869 Blood Head T-shirt, black 10869 40.00 0.00 0 0 0 blood_head_tee_black
10872 Outlaw T-shirt, black 10872 40.00 0.00 0 0 0 outlaw_tee_black
10874 Random T-shirt, black 10874 40.00 0.00 0 0 0 random_tee_black

buttons_group, buttons_dropdown_after, active switch

Id Name Sold Actions
10051 Archives T-shirt, black 270
10052 Archives T-shirt, navy 719
10053 Archives T-shirt, purple 993
10056 Attacker T-shirt, white 412
10058 Blamo T-shirt, black 208
10059 Blamo T-shirt, white 518

sticky_header => true (page scroll)

Id Name Price Sold
10051 Archives T-shirt, black 37.00 270
10052 Archives T-shirt, navy 37.00 719
10053 Archives T-shirt, purple 37.00 993
10056 Attacker T-shirt, white 37.00 412
10058 Blamo T-shirt, black 37.00 208
10059 Blamo T-shirt, white 37.00 518
10060 Blamo T-shirt, black and green 37.00 861
10061 Paint Bucket T-shirt, green 37.00 1009
10062 Paint Bucket T-shirt, white 37.00 559
10063 Paint Bucket T-shirt, purple 37.00 521
10066 Flash T-shirt, white 35.00 90
10067 Mix Tape T-shirt, black 45.00 826
10245 Winner T-shirt, grey 35.00 787
10247 Hill T-shirt, white 35.00 518
10865 Roseheart T-shirt, black 35.00 0
10866 Liberty T-shirt, black 35.00 0
10868 Pray Skull T-shirt, black 35.00 0
10869 Blood Head T-shirt, black 40.00 0
10872 Outlaw T-shirt, black 40.00 0
10874 Random T-shirt, black 40.00 0

Table variants (table_class_add, thead, tbody)

Id Name Price
10051 Archives T-shirt, black 37.00
10052 Archives T-shirt, navy 37.00
10053 Archives T-shirt, purple 37.00
10056 Attacker T-shirt, white 37.00
table-striped-columns, caption-top, a table-dark head and a table-group-divider body
Id Name Price
10058 Blamo T-shirt, black 37.00
10059 Blamo T-shirt, white 37.00
10060 Blamo T-shirt, black and green 37.00

html()->pagination() states and sizes

size sm, align start, current 2, prev disabled

size default, align center, current 2, prev disabled

size lg, align end, current 2, prev disabled

pager_size, pager_align

Id Name Price
10059 Blamo T-shirt, white 37.00
10060 Blamo T-shirt, black and green 37.00
10061 Paint Bucket T-shirt, green 37.00
10062 Paint Bucket T-shirt, white 37.00
10063 Paint Bucket T-shirt, purple 37.00