Domma Themes

Beautiful, consistent theming with light & dark modes across 11 colour families. Zero configuration required.

Choose a Theme

Click any theme to preview it instantly. Changes apply to the entire page.

Choose a Theme
Aa
Ocean
β˜€οΈ Light
Aa
Ocean
πŸŒ™ Dark
Aa
Forest
β˜€οΈ Light
Aa
Forest
πŸŒ™ Dark
Aa
Sunset
β˜€οΈ Light
Aa
Sunset
πŸŒ™ Dark
Aa
Royal
β˜€οΈ Light
Aa
Royal
πŸŒ™ Dark
Aa
Lemon
β˜€οΈ Light
Aa
Lemon
πŸŒ™ Dark
Aa
Silver
β˜€οΈ Light
Aa
Silver
πŸŒ™ Dark
Aa
Charcoal
β˜€οΈ Light
Aa
Charcoal
πŸŒ™ Dark
Aa
Christmas
❄️ Day
Aa
Christmas
πŸŽ„ Eve
Aa
Unicorn
β˜€οΈ Light
Aa
Unicorn
πŸŒ™ Dark
Aa
Dreamy
β˜€οΈ Light
Aa
Dreamy
πŸŒ™ Dark
Aa
Grayve
β˜€οΈ Light
Aa
Grayve
πŸŒ™ Dark
Aa
Mint
β˜€οΈ Light
Aa
Mint
πŸŒ™ Dark
Aa
Wedding
β˜€οΈ Day
Aa
Wedding
πŸŒ™ Evening
Aa
Core
β˜€οΈ Light only
Aa
Corporate
β˜€οΈ Light
Aa
Corporate
πŸŒ™ Dark
Admin Smooth
Steel
Admin Slate
Steel
Admin Sharp
Steel
Admin Smooth
Indigo
Admin Slate
Indigo
Admin Sharp
Indigo
Admin Smooth
Teal
Admin Slate
Teal
Admin Sharp
Teal
<div class="theme-grid"><div class="theme-pair">
        <div class="theme-card" data-theme="ocean-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #e0f7fa, #b2ebf2);">
                <span style="color: #006064;">Aa</span>
                <div class="theme-card-accent" style="background: #0077b6;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Ocean</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="ocean-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #023e8a, #0077b6);">
                <span style="color: #caf0f8;">Aa</span>
                <div class="theme-card-accent" style="background: #00b4d8;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Ocean</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="forest-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #e8f5e9, #c8e6c9);">
                <span style="color: #1b5e20;">Aa</span>
                <div class="theme-card-accent" style="background: #2d6a4f;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Forest</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="forest-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1b4332, #2d6a4f);">
                <span style="color: #95d5b2;">Aa</span>
                <div class="theme-card-accent" style="background: #52b788;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Forest</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="sunset-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #fff3e0, #ffe0b2);">
                <span style="color: #bf360c;">Aa</span>
                <div class="theme-card-accent" style="background: #c67b5c;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Sunset</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="sunset-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #2d1b12, #4a2c1a);">
                <span style="color: #e8c9a8;">Aa</span>
                <div class="theme-card-accent" style="background: #c67b5c;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Sunset</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="royal-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #ede7f6, #d1c4e9);">
                <span style="color: #4a148c;">Aa</span>
                <div class="theme-card-accent" style="background: #7c3aed;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Royal</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="royal-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1e3a8a, #2b4c9e);">
                <span style="color: #c7d2fe;">Aa</span>
                <div class="theme-card-accent" style="background: #6495ed;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Royal</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="lemon-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #fffde7, #fff9c4);">
                <span style="color: #f57f17;">Aa</span>
                <div class="theme-card-accent" style="background: #fbbf24;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Lemon</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="lemon-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1a1200, #2d2000);">
                <span style="color: #fef3c7;">Aa</span>
                <div class="theme-card-accent" style="background: #d97706;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Lemon</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="silver-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #fafafa, #eeeeee);">
                <span style="color: #424242;">Aa</span>
                <div class="theme-card-accent" style="background: #6b7280;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Silver</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="silver-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1a1f2e, #252d3d);">
                <span style="color: #e2e8f0;">Aa</span>
                <div class="theme-card-accent" style="background: #94a3b8;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Silver</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="charcoal-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #f5f5f5, #e0e0e0);">
                <span style="color: #212121;">Aa</span>
                <div class="theme-card-accent" style="background: #78909c;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Charcoal</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="charcoal-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1a202c, #2d3748);">
                <span style="color: #e2e8f0;">Aa</span>
                <div class="theme-card-accent" style="background: #78909c;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Charcoal</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="christmas-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #F5F8FC, #EDF2F8);">
                <span style="color: #1A2A1E;">Aa</span>
                <div class="theme-card-accent" style="background: #2D6B3F;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Christmas</div>
                <div class="theme-card-mode">❄️ Day</div>
            </div>
        </div>
        <div class="theme-card" data-theme="christmas-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #0A1A10, #1A3020);">
                <span style="color: #F0ECE4;">Aa</span>
                <div class="theme-card-accent" style="background: #5CB875;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Christmas</div>
                <div class="theme-card-mode">πŸŽ„ Eve</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="unicorn-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #fce4ec, #f8bbd0);">
                <span style="color: #880e4f;">Aa</span>
                <div class="theme-card-accent" style="background: #e91e90;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Unicorn</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="unicorn-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1a0e24, #4a2660);">
                <span style="color: #f8bbd0;">Aa</span>
                <div class="theme-card-accent" style="background: #e91e90;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Unicorn</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="dreamy-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #efebe9, #d7ccc8);">
                <span style="color: #3e2723;">Aa</span>
                <div class="theme-card-accent" style="background: #8d6e63;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Dreamy</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="dreamy-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1c1410, #4e342e);">
                <span style="color: #d7ccc8;">Aa</span>
                <div class="theme-card-accent" style="background: #a1887f;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Dreamy</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="grayve-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #f8f9fa, #e0f7fa);">
                <span style="color: #006064;">Aa</span>
                <div class="theme-card-accent" style="background: #00838f;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Grayve</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="grayve-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #1a1e21, #343A40);">
                <span style="color: #DEE2E6;">Aa</span>
                <div class="theme-card-accent" style="background: #00bcd4;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Grayve</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="mint-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #f0faf5, #d4f5e4);">
                <span style="color: #1a2e25;">Aa</span>
                <div class="theme-card-accent" style="background: #2e9e6e;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Mint</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="mint-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #0f172a, #1e3a2f);">
                <span style="color: #d4f5e4;">Aa</span>
                <div class="theme-card-accent" style="background: #6dd5a8;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Mint</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="wedding-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #FFFDF7, #FDF5E0);">
                <span style="color: #2C2416;">Aa</span>
                <div class="theme-card-accent" style="background: #C5A55A;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Wedding</div>
                <div class="theme-card-mode">β˜€οΈ Day</div>
            </div>
        </div>
        <div class="theme-card" data-theme="wedding-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #12121F, #1A1A2E);">
                <span style="color: #F0E8D8;">Aa</span>
                <div class="theme-card-accent" style="background: #C5A55A;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Wedding</div>
                <div class="theme-card-mode">πŸŒ™ Evening</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="core-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #ffffff, #f3f4f6);">
                <span style="color: #111827;">Aa</span>
                <div class="theme-card-accent" style="background: #4f46e5;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Core</div>
                <div class="theme-card-mode">β˜€οΈ Light only</div>
            </div>
        </div>
    </div>
    <div class="theme-pair">
        <div class="theme-card" data-theme="corporate-light">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #f8fafc, #ffffff);">
                <span style="color: #0f172a;">Aa</span>
                <div class="theme-card-accent" style="background: #1e40af;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Corporate</div>
                <div class="theme-card-mode">β˜€οΈ Light</div>
            </div>
        </div>
        <div class="theme-card" data-theme="corporate-dark">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #0b0f19, #151c2c);">
                <span style="color: #f8fafc;">Aa</span>
                <div class="theme-card-accent" style="background: #2563eb;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Corporate</div>
                <div class="theme-card-mode">πŸŒ™ Dark</div>
            </div>
        </div>
    </div>
    <div class="theme-pair theme-trio">
        <div class="theme-card" data-theme="admin-smooth-steel">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #353a43, #3c414a);">
                <div class="theme-card-accent" style="background: #3b76bc;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Smooth</div>
                <div class="theme-card-mode">Steel</div>
            </div>
        </div>
        <div class="theme-card" data-theme="admin-slate-steel">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #252c37, #2e3644);">
                <div class="theme-card-accent" style="background: #3b76bc;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Slate</div>
                <div class="theme-card-mode">Steel</div>
            </div>
        </div>
        <div class="theme-card" data-theme="admin-sharp-steel">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #eef1f6, #283242);">
                <div class="theme-card-accent" style="background: #3b76bc;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Sharp</div>
                <div class="theme-card-mode">Steel</div>
            </div>
        </div>
    </div>
    <div class="theme-pair theme-trio">
        <div class="theme-card" data-theme="admin-smooth-indigo">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #353a43, #3c414a);">
                <div class="theme-card-accent" style="background: #5b63a8;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Smooth</div>
                <div class="theme-card-mode">Indigo</div>
            </div>
        </div>
        <div class="theme-card" data-theme="admin-slate-indigo">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #252c37, #2e3644);">
                <div class="theme-card-accent" style="background: #5b63a8;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Slate</div>
                <div class="theme-card-mode">Indigo</div>
            </div>
        </div>
        <div class="theme-card" data-theme="admin-sharp-indigo">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #eef1f6, #283242);">
                <div class="theme-card-accent" style="background: #5b63a8;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Sharp</div>
                <div class="theme-card-mode">Indigo</div>
            </div>
        </div>
    </div>
    <div class="theme-pair theme-trio">
        <div class="theme-card" data-theme="admin-smooth-teal">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #353a43, #3c414a);">
                <div class="theme-card-accent" style="background: #2a8178;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Smooth</div>
                <div class="theme-card-mode">Teal</div>
            </div>
        </div>
        <div class="theme-card" data-theme="admin-slate-teal">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #252c37, #2e3644);">
                <div class="theme-card-accent" style="background: #2a8178;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Slate</div>
                <div class="theme-card-mode">Teal</div>
            </div>
        </div>
        <div class="theme-card" data-theme="admin-sharp-teal">
            <div class="theme-card-preview" style="background: linear-gradient(135deg, #eef1f6, #283242);">
                <div class="theme-card-accent" style="background: #2a8178;"></div>
            </div>
            <div class="theme-card-body">
                <div class="theme-card-name">Admin Sharp</div>
                <div class="theme-card-mode">Teal</div>
            </div>
        </div>
    </div>
</div>
/**
 * Choose a Theme - from public/showcase/themes.
 */
export function mount(root) {
    function updateActiveStates() {
        const current = Domma.theme.get();
        $('.theme-card').removeClass('active');
        $(`.theme-card[data-theme="${current}"]`).addClass('active');
      }
    $('.theme-card').on('click', function () {
        Domma.theme.set($(this).data('theme'));
      });
    updateActiveStates();
}

Admin Theme Builder

Pick a finish, then an accent. Nine looks, two clicks.

Admin Theme Builder
1 Β· Finish
2 Β· Accent
Theme: admin-sharp-steel
Revenue
Β£48k
Orders
1,204
Active
317
Active
<div class="dm-card" style="padding: var(--dm-space-5);">
    <div class="dm-label">1 Β· Finish</div>
    <div class="btn-group" id="admin-finish" role="group" aria-label="Finish" style="display:flex; gap:8px; margin-bottom:20px;">
        <button type="button" class="btn" data-finish="smooth">Smooth</button>
        <button type="button" class="btn" data-finish="slate">Slate</button>
        <button type="button" class="btn active" data-finish="sharp">Sharp</button>
    </div>
    <div class="dm-label">2 Β· Accent</div>
    <div id="admin-accent" style="display:flex; gap:12px; margin-top:8px;">
        <button type="button" class="admin-acc active" data-accent="steel" data-tooltip="Steel Blue" aria-label="Steel Blue accent" style="flex:1; height:44px; border-radius:8px; border:2px solid var(--dm-text); background:#3b76bc;"></button>
        <button type="button" class="admin-acc" data-accent="indigo" data-tooltip="Indigo" aria-label="Indigo accent" style="flex:1; height:44px; border-radius:8px; border:2px solid transparent; background:#5b63a8;"></button>
        <button type="button" class="admin-acc" data-accent="teal" data-tooltip="Teal" aria-label="Teal accent" style="flex:1; height:44px; border-radius:8px; border:2px solid transparent; background:#2a8178;"></button>
    </div>
    <div class="dm-mt-4 dm-text-sm dm-text-secondary">Theme: <code id="admin-current">admin-sharp-steel</code></div>
</div>

<div class="dm-card" id="admin-preview" style="padding:0; overflow:hidden;">
    <div style="display:flex; min-height:300px;">
        <aside style="width:160px; background:var(--dm-sidebar-bg); color:var(--dm-sidebar-text); padding:16px;">
            <strong style="color:var(--dm-text-inverse,#fff); display:block; margin-bottom:16px;">Admin</strong>
            <div style="background:var(--dm-primary); color:var(--dm-primary-text, #fff); padding:8px; border-radius:6px; margin-bottom:4px;">Dashboard</div>
            <div style="padding:8px;">Users</div>
            <div style="padding:8px;">Reports</div>
        </aside>
        <div style="flex:1; background:var(--dm-background); padding:16px;">
            <div class="dm-grid dm-gap-3 dm-mb-4" style="grid-template-columns:repeat(3,1fr);">
                <div class="dm-card" style="padding:12px;"><div class="dm-text-xs dm-text-muted">Revenue</div><div style="font-size:20px; font-weight:700;">Β£48k</div></div>
                <div class="dm-card" style="padding:12px;"><div class="dm-text-xs dm-text-muted">Orders</div><div style="font-size:20px; font-weight:700;">1,204</div></div>
                <div class="dm-card" style="padding:12px;"><div class="dm-text-xs dm-text-muted">Active</div><div style="font-size:20px; font-weight:700;">317</div></div>
            </div>
            <div id="admin-preview-table"></div>
            <div class="dm-mt-4" style="display:flex; gap:8px; align-items:center;">
                <button type="button" class="btn btn-primary">Save</button>
                <button type="button" class="btn">Cancel</button>
                <span class="badge badge-success">Active</span>
            </div>
        </div>
    </div>
</div>
/**
 * Admin Theme Builder - from public/showcase/themes.
 */
export function mount(root) {
    E.tooltip('[data-tooltip]');
      // --- Admin Theme Builder ---
    (function initAdminBuilder() {
        const state = { finish: 'sharp', accent: 'steel' };
        const $current = $(root).find('#admin-current');
        const $finish = $(root).find('#admin-finish');
        const $accent = $(root).find('#admin-accent');
        if (!$finish.length || !$accent.length || !$current.length) return;

        // Wire real tooltips so data-tooltip isn't dead markup (one instance per button)
        if (window.Domma && Domma.elements && E.tooltip) {
          $accent.find('.admin-acc').each((i, btn) => {
            E.tooltip(btn, { trigger: 'hover', position: 'top' });
          });
        }

        if (window.Domma && Domma.tables) {
          Domma.tables.create('#admin-preview-table', {
            data: [
              { customer: 'Acme Ltd', plan: 'Pro', status: 'Active' },
              { customer: 'Globex', plan: 'Team', status: 'Trial' },
              { customer: 'Initech', plan: 'Free', status: 'Active' }
            ],
            columns: [
              { key: 'customer', title: 'Customer' },
              { key: 'plan', title: 'Plan' },
              { key: 'status', title: 'Status' }
            ],
            pagination: false
          });
        }

        function apply() {
          const name = `admin-${state.finish}-${state.accent}`;
          $current.text(name);
          Domma.theme.set(name);
        }

        $finish.on('click', '[data-finish]', function () {
          state.finish = $(this).data('finish');
          $finish.find('[data-finish]').removeClass('active');
          $(this).addClass('active');
          apply();
        });

        $accent.on('click', '[data-accent]', function () {
          state.accent = $(this).data('accent');
          $accent.find('[data-accent]').css('border-color', 'transparent');
          $(this).css('border-color', 'var(--dm-text)');
          apply();
        });
      })();
}
Admin Theme Builder
Live preview

Switch themes above; every component on this page reflects your choice instantly.

Audit off
<div class="audit-bar"><div>
        <strong>Live preview</strong>
        <p class="text-muted" style="margin: 0; font-size: 0.85rem;">Switch themes above; every component on this page reflects your choice instantly.</p>
    </div>
    <div class="audit-actions">
        <span class="audit-summary idle" id="audit-summary">Audit off</span>
        <button type="button" class="audit-toggle" id="audit-toggle">
            <span data-icon="eye" data-icon-size="16"></span>
            <span id="audit-toggle-text">Highlight contrast issues</span>
        </button>
        <button type="button" class="audit-toggle" id="audit-dump" title="Copy failures to clipboard for sharing">
            <span data-icon="clipboard" data-icon-size="16"></span>
            <span>Copy failures</span>
        </button>
    </div>
</div>
/**
 * Admin Theme Builder - from public/showcase/themes.
 */
export function mount(root) {
    const colours = [
          {name: 'Primary',    var: '--dm-primary'},
          {name: 'Secondary',  var: '--dm-secondary'},
          {name: 'Success',    var: '--dm-success'},
          {name: 'Danger',     var: '--dm-danger'},
          {name: 'Warning',    var: '--dm-warning'},
          {name: 'Info',       var: '--dm-info'},
          {name: 'Background', var: '--dm-background'},
          {name: 'Surface',    var: '--dm-surface'},
          {name: 'Text',       var: '--dm-text'},
          {name: 'Border',     var: '--dm-border'}
      ];
      // ── Contrast checker (WCAG AA) ────────────────────────────────────────
    let auditEnabled = false;
    const $toggleBtn = $(root).find('#audit-toggle');
    const $toggleText = $(root).find('#audit-toggle-text');
    const $summary = $(root).find('#audit-summary');
    function parseRgb(str) {
        if (!str) return null;
        const m = str.match(/rgba?\(([^)]+)\)/);
        if (!m) return null;
        const parts = m[1].split(',').map(p => parseFloat(p.trim()));
        if (parts.length < 3) return null;
        const a = parts[3] === undefined ? 1 : parts[3];
        return { r: parts[0], g: parts[1], b: parts[2], a };
      }
    function relLuminance(c) {
        const norm = v => {
          v /= 255;
          return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
        };
        return 0.2126 * norm(c.r) + 0.7152 * norm(c.g) + 0.0722 * norm(c.b);
      }
    function contrastRatio(fg, bg) {
        const l1 = relLuminance(fg);
        const l2 = relLuminance(bg);
        const [light, dark] = l1 > l2 ? [l1, l2] : [l2, l1];
        return (light + 0.05) / (dark + 0.05);
      }
      // Alpha-composite "src" over "dst" using standard "over" operator.
      // Returns an opaque colour (a=1) approximating the rendered result.
    function composite(src, dst) {
        if (!src) return dst;
        if (src.a >= 0.999) return { r: src.r, g: src.g, b: src.b, a: 1 };
        const a = src.a + dst.a * (1 - src.a);
        const r = (src.r * src.a + dst.r * dst.a * (1 - src.a)) / (a || 1);
        const g = (src.g * src.a + dst.g * dst.a * (1 - src.a)) / (a || 1);
        const b = (src.b * src.a + dst.b * dst.a * (1 - src.a)) / (a || 1);
        return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a: 1 };
      }
    function effectiveBg(el) {
        // Walk up the ancestor chain collecting every translucent layer, then
        // composite them top-down so a `rgba(brand, 0.15)` over a dark surface
        // resolves to its real visual colour rather than the brand colour itself.
        const layers = [];
        let cur = el;
        while (cur && cur !== document.documentElement) {
          const bg = parseRgb(getComputedStyle(cur).backgroundColor);
          if (bg && bg.a > 0) {
            layers.push(bg);
            if (bg.a >= 0.999) break;
          }
          cur = cur.parentElement;
        }
        let base = parseRgb(getComputedStyle(document.body).backgroundColor) || { r: 255, g: 255, b: 255, a: 1 };
        if (!base.a || base.a < 0.999) base = { r: base.r || 255, g: base.g || 255, b: base.b || 255, a: 1 };
        // layers[0] is the element itself, layers[N] is closest opaque ancestor; composite from back to front
        for (let i = layers.length - 1; i >= 0; i--) base = composite(layers[i], base);
        return base;
      }
    function hasOwnText(el) {
        return Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim().length > 1);
      }
    function clearAuditMarks() {
        $('.audit-fail').removeClass('audit-fail').removeAttr('data-audit-ratio');
      }
      // Web-component hosts present known false-positives for background detection
      // (their Shadow DOM slots reparent children for layout while keeping them in
      // light DOM, which confuses ancestor-walk-based bg detection). Skip elements
      // inside these wrappers; the underlying button colours are tested elsewhere.
    function isInsideUnauditedHost(el) {
        let cur = el;
        while (cur && cur !== document.body) {
          const tag = cur.tagName ? cur.tagName.toLowerCase() : '';
          if (tag === 'domma-tooltip' || tag === 'domma-number-badge') return true;
          cur = cur.parentElement;
        }
        return false;
      }
    function runAudit() {
        clearAuditMarks();
        let failures = 0;
        $('.audit-section *').each((i, el) => {
          if (!hasOwnText(el)) return;
          if (isInsideUnauditedHost(el)) return;
          const styles = getComputedStyle(el);
          if (styles.visibility === 'hidden' || styles.display === 'none') return;
          const fg = parseRgb(styles.color);
          const bg = effectiveBg(el);
          if (!fg || !bg || fg.a < 0.1) return;

          const ratio = contrastRatio(fg, bg);
          const fontSize = parseFloat(styles.fontSize);
          const fontWeight = parseInt(styles.fontWeight) || 400;
          const isLarge = fontSize >= 24 || (fontSize >= 18.66 && fontWeight >= 700);
          const threshold = isLarge ? 3 : 4.5;

          if (ratio < threshold) {
            el.classList.add('audit-fail');
            el.setAttribute('data-audit-ratio', ratio.toFixed(2));
            failures++;
          }
        });

        $summary.removeClass('idle has-issues clean');
        if (failures === 0) {
          $summary.text('No issues found').addClass('clean');
        } else {
          $summary.text(`${failures} contrast issue${failures === 1 ? '' : 's'}`).addClass('has-issues');
        }
      }
    $toggleBtn.on('click', () => {
        auditEnabled = !auditEnabled;
        $toggleBtn.toggleClass('active', auditEnabled);
        $toggleText.text(auditEnabled ? 'Hide contrast issues' : 'Highlight contrast issues');
        if (auditEnabled) {
          runAudit();
        } else {
          clearAuditMarks();
          $summary.removeClass('has-issues clean').addClass('idle').text('Audit off');
        }
      });
      // ── Dump failures ─────────────────────────────────────────────────────
      // Collects every failing element with its CSS-path-style selector and
      // copies a structured report to the clipboard for sharing.
    function describeElement(el) {
        const parts = [];
        let cur = el;
        while (cur && cur !== document.body && parts.length < 5) {
          let s = cur.tagName.toLowerCase();
          if (cur.id) s += `#${cur.id}`;
          else if (cur.className && typeof cur.className === 'string') {
            const classes = cur.className.trim().split(/\s+/).slice(0, 4).join('.');
            if (classes) s += `.${classes}`;
          }
          parts.unshift(s);
          cur = cur.parentElement;
        }
        return parts.join(' > ');
      }
    $(root).find('#audit-dump').on('click', async () => {
        let report = '';
        try {
          const allThemes = (Domma.theme && Domma.theme.listThemes) ? Domma.theme.listThemes() : [];
          const originalTheme = (Domma.theme && Domma.theme.get) ? Domma.theme.get() : null;
          const wasEnabled = auditEnabled;

          report = `# Contrast failure dump - generated ${D().toISOString()}\n`;
          report += `Themes scanned: ${allThemes.length}\n\n`;

          const summary = [];

          for (const theme of allThemes) {
            // Apply theme and let CSS settle before measuring
            if (Domma.theme && Domma.theme.set) Domma.theme.set(theme);
            // Two RAFs + a settle delay so style/layout recalculation finishes
            // before the audit reads computed values; otherwise dumps occasionally
            // capture transitional state (e.g. half-applied theme classes).
            await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
            await new Promise(r => setTimeout(r, 200));

            clearAuditMarks();
            runAudit();

            const fails = [];
            $('.audit-fail').each((i, el) => {
              try {
                const styles = getComputedStyle(el);
                const fg = parseRgb(styles.color);
                const bg = effectiveBg(el);
                fails.push({
                  ratio: parseFloat(el.getAttribute('data-audit-ratio')) || 0,
                  path: describeElement(el),
                  text: (el.textContent || '').trim().slice(0, 60).replace(/\s+/g, ' '),
                  fg: fg ? `rgb(${fg.r},${fg.g},${fg.b})` : (styles.color || 'unknown'),
                  bg: bg ? `rgb(${bg.r},${bg.g},${bg.b})` : 'unknown',
                  fontSize: parseFloat(styles.fontSize) || 0,
                  fontWeight: parseInt(styles.fontWeight) || 400
                });
              } catch (innerErr) {
                fails.push({ ratio: 0, path: '(error)', text: String(innerErr.message || innerErr), fg: '?', bg: '?', fontSize: 0, fontWeight: 0 });
              }
            });
            fails.sort((a, b) => a.ratio - b.ratio);
            summary.push({ theme, count: fails.length });

            report += `\n══════════════════════════════════════════════\n`;
            report += `THEME: ${theme}  -  ${fails.length} failures\n`;
            report += `══════════════════════════════════════════════\n`;
            // Limit to 30 worst per theme to keep report manageable
            const sample = fails.slice(0, 30);
            sample.forEach((f, i) => {
              report += `${i + 1}. [${f.ratio.toFixed(2)}:1] ${f.path}\n`;
              report += `   text: "${f.text}"\n   fg=${f.fg}  bg=${f.bg}  ${f.fontSize}px/${f.fontWeight}\n`;
            });
            if (fails.length > sample.length) {
              report += `... and ${fails.length - sample.length} more (truncated)\n`;
            }
          }

          // Restore original theme
          if (originalTheme && Domma.theme && Domma.theme.set) Domma.theme.set(originalTheme);

          if (!wasEnabled) {
            clearAuditMarks();
            summaryEl.classList.remove('has-issues', 'clean');
            summaryEl.classList.add('idle');
            summaryEl.textContent = 'Audit off';
          } else {
            runAudit();
          }

          const totalReport = '# SUMMARY\n' + summary.map(s => `  ${s.theme.padEnd(20)} ${s.count}`).join('\n') + '\n\n' + report;
          report = totalReport;
          console.log(report);
        } catch (err) {
          report = '# DUMP ERROR\n' + (err && err.stack ? err.stack : String(err));
          console.error(report);
        }

        // Show a modal-style textarea with the report so the user can copy it
        // regardless of clipboard API availability.
        // .remove() on an empty collection is a no-op, so no guard is needed.
        $(root).find('#audit-dump-modal').remove();

        const overlay = document.createElement('div');
        overlay.id = 'audit-dump-modal';
        Object.assign(overlay.style, {
          position: 'fixed', inset: '0',
          background: 'rgba(0,0,0,0.55)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          zIndex: '99999', padding: '2rem'
        });

        const panel = document.createElement('div');
        Object.assign(panel.style, {
          background: '#fff', color: '#000',
          borderRadius: '8px',
          maxWidth: '800px', maxHeight: '80vh', width: '100%',
          display: 'flex', flexDirection: 'column', overflow: 'hidden',
          boxShadow: '0 12px 40px rgba(0,0,0,0.4)'
        });

        const header = document.createElement('div');
        header.style.cssText = 'padding: 0.75rem 1rem; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center;';
        const title = document.createElement('strong');
        title.textContent = 'Contrast failure dump (Ctrl+A then Ctrl+C to copy)';
        const closeBtn = document.createElement('button');
        closeBtn.textContent = 'Γ—';
        closeBtn.style.cssText = 'background: none; border: none; font-size: 1.5rem; cursor: pointer; line-height: 1; color: #000;';
        $(closeBtn).on('click', () => overlay.remove());
        header.appendChild(title);
        header.appendChild(closeBtn);

        const ta = document.createElement('textarea');
        ta.value = report;
        ta.readOnly = true;
        Object.assign(ta.style, {
          flex: '1', width: '100%', border: 'none',
          padding: '1rem', fontFamily: 'monospace', fontSize: '12px',
          resize: 'none', outline: 'none', background: '#f8f9fa', color: '#000'
        });

        panel.appendChild(header);
        panel.appendChild(ta);
        overlay.appendChild(panel);
        document.body.appendChild(overlay);
        $(overlay).on('click', (e) => { if (e.target === overlay) overlay.remove(); });
        setTimeout(() => { ta.focus(); ta.select(); }, 50);

        // Best-effort attempt at modern clipboard, but never block the modal.
        if (navigator.clipboard && navigator.clipboard.writeText) {
          navigator.clipboard.writeText(report).catch(() => {});
        }
      });
}

1. Branding & Surfaces

Brand colours (text on coloured background)

1. Branding & Surfaces
Primary
Secondary
Success
Warning
Danger
Info
<div class="branding-strip"><div class="swatch-primary">Primary</div>
    <div class="swatch-secondary">Secondary</div>
    <div class="swatch-success">Success</div>
    <div class="swatch-warning">Warning</div>
    <div class="swatch-danger">Danger</div>
    <div class="swatch-info">Info</div>
</div>

Surface tokens (text on neutral backgrounds)

1. Branding & Surfaces
Background - body text
Surface - card text
Surface alt - subtle text
Surface raised - elevated text
<div class="surface-strip"><div class="surf-bg">Background - body text</div>
    <div class="surf-surface">Surface - card text</div>
    <div class="surf-alt">Surface alt - subtle text</div>
    <div class="surf-raised">Surface raised - elevated text</div>
</div>

Colour palette (every theme token)

1. Branding & Surfaces
<div id="colour-palette" class="themes-colour-palette"></div>
/**
 * 1. Branding & Surfaces - from public/showcase/themes.
 */
export function mount(root) {
    const colours = [
          {name: 'Primary',    var: '--dm-primary'},
          {name: 'Secondary',  var: '--dm-secondary'},
          {name: 'Success',    var: '--dm-success'},
          {name: 'Danger',     var: '--dm-danger'},
          {name: 'Warning',    var: '--dm-warning'},
          {name: 'Info',       var: '--dm-info'},
          {name: 'Background', var: '--dm-background'},
          {name: 'Surface',    var: '--dm-surface'},
          {name: 'Text',       var: '--dm-text'},
          {name: 'Border',     var: '--dm-border'}
      ];
    function renderPalette() {
        const $host = $(root).find('#colour-palette');
        if (!$host.length) return;
        const host = $host.empty()[0];
        colours.forEach(c => {
          const swatch = document.createElement('div');
          swatch.className = 'themes-colour-swatch';
          const circle = document.createElement('div');
          circle.className = 'themes-colour-circle';
          circle.style.background = `var(${c.var})`;
          const label = document.createElement('div');
          label.className = 'themes-colour-label';
          label.textContent = c.name;
          swatch.appendChild(circle);
          swatch.appendChild(label);
          host.appendChild(swatch);
        });
      }
    function updateActiveStates() {
        const current = Domma.theme.get();
        $('.theme-card').removeClass('active');
        $(`.theme-card[data-theme="${current}"]`).addClass('active');
      }
    renderPalette();
    Domma.theme.onChange(() => {
        updateActiveStates();
        setTimeout(() => {
          renderPalette();
          if (auditEnabled) runAudit();
        }, 80);
      });
      // ── Contrast checker (WCAG AA) ────────────────────────────────────────
    let auditEnabled = false;
    function parseRgb(str) {
        if (!str) return null;
        const m = str.match(/rgba?\(([^)]+)\)/);
        if (!m) return null;
        const parts = m[1].split(',').map(p => parseFloat(p.trim()));
        if (parts.length < 3) return null;
        const a = parts[3] === undefined ? 1 : parts[3];
        return { r: parts[0], g: parts[1], b: parts[2], a };
      }
    function relLuminance(c) {
        const norm = v => {
          v /= 255;
          return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
        };
        return 0.2126 * norm(c.r) + 0.7152 * norm(c.g) + 0.0722 * norm(c.b);
      }
    function contrastRatio(fg, bg) {
        const l1 = relLuminance(fg);
        const l2 = relLuminance(bg);
        const [light, dark] = l1 > l2 ? [l1, l2] : [l2, l1];
        return (light + 0.05) / (dark + 0.05);
      }
      // Alpha-composite "src" over "dst" using standard "over" operator.
      // Returns an opaque colour (a=1) approximating the rendered result.
    function composite(src, dst) {
        if (!src) return dst;
        if (src.a >= 0.999) return { r: src.r, g: src.g, b: src.b, a: 1 };
        const a = src.a + dst.a * (1 - src.a);
        const r = (src.r * src.a + dst.r * dst.a * (1 - src.a)) / (a || 1);
        const g = (src.g * src.a + dst.g * dst.a * (1 - src.a)) / (a || 1);
        const b = (src.b * src.a + dst.b * dst.a * (1 - src.a)) / (a || 1);
        return { r: Math.round(r), g: Math.round(g), b: Math.round(b), a: 1 };
      }
    function effectiveBg(el) {
        // Walk up the ancestor chain collecting every translucent layer, then
        // composite them top-down so a `rgba(brand, 0.15)` over a dark surface
        // resolves to its real visual colour rather than the brand colour itself.
        const layers = [];
        let cur = el;
        while (cur && cur !== document.documentElement) {
          const bg = parseRgb(getComputedStyle(cur).backgroundColor);
          if (bg && bg.a > 0) {
            layers.push(bg);
            if (bg.a >= 0.999) break;
          }
          cur = cur.parentElement;
        }
        let base = parseRgb(getComputedStyle(document.body).backgroundColor) || { r: 255, g: 255, b: 255, a: 1 };
        if (!base.a || base.a < 0.999) base = { r: base.r || 255, g: base.g || 255, b: base.b || 255, a: 1 };
        // layers[0] is the element itself, layers[N] is closest opaque ancestor; composite from back to front
        for (let i = layers.length - 1; i >= 0; i--) base = composite(layers[i], base);
        return base;
      }
    function hasOwnText(el) {
        return Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim().length > 1);
      }
    function clearAuditMarks() {
        $('.audit-fail').removeClass('audit-fail').removeAttr('data-audit-ratio');
      }
      // Web-component hosts present known false-positives for background detection
      // (their Shadow DOM slots reparent children for layout while keeping them in
      // light DOM, which confuses ancestor-walk-based bg detection). Skip elements
      // inside these wrappers; the underlying button colours are tested elsewhere.
    function isInsideUnauditedHost(el) {
        let cur = el;
        while (cur && cur !== document.body) {
          const tag = cur.tagName ? cur.tagName.toLowerCase() : '';
          if (tag === 'domma-tooltip' || tag === 'domma-number-badge') return true;
          cur = cur.parentElement;
        }
        return false;
      }
    function runAudit() {
        clearAuditMarks();
        let failures = 0;
        $('.audit-section *').each((i, el) => {
          if (!hasOwnText(el)) return;
          if (isInsideUnauditedHost(el)) return;
          const styles = getComputedStyle(el);
          if (styles.visibility === 'hidden' || styles.display === 'none') return;
          const fg = parseRgb(styles.color);
          const bg = effectiveBg(el);
          if (!fg || !bg || fg.a < 0.1) return;

          const ratio = contrastRatio(fg, bg);
          const fontSize = parseFloat(styles.fontSize);
          const fontWeight = parseInt(styles.fontWeight) || 400;
          const isLarge = fontSize >= 24 || (fontSize >= 18.66 && fontWeight >= 700);
          const threshold = isLarge ? 3 : 4.5;

          if (ratio < threshold) {
            el.classList.add('audit-fail');
            el.setAttribute('data-audit-ratio', ratio.toFixed(2));
            failures++;
          }
        });

        $summary.removeClass('idle has-issues clean');
        if (failures === 0) {
          $summary.text('No issues found').addClass('clean');
        } else {
          $summary.text(`${failures} contrast issue${failures === 1 ? '' : 's'}`).addClass('has-issues');
        }
      }
}

2. Typography

H3 - Engineering team headcount

H4 - Customer satisfaction up four points

H5 - Migration completed last Tuesday

H6 - Last updated by Darryl on 22 April 2026

Standard paragraph text - the rain in Spain falls mainly on the plain. This is a hyperlink with an inline strong, an inline emphasis, and some inline code. Visited links should also have legible colour.

"Design is not just what it looks like and feels like. Design is how it works." - Steve Jobs

Muted helper text - usually used for form hints or metadata.

Secondary text - slightly de-emphasised body copy.

const order = { id: 'INV-2026-0042', customer: 'Acme Ltd', total: 4250.00 };
Domma.http.post('/api/invoices', order).then(res => {
  E.toast('Invoice saved', { type: 'success' });
});

3. Buttons

Solid variants

3. Buttons
<div class="audit-row"><button type="button" class="btn btn-primary">Primary</button>
    <button type="button" class="btn btn-secondary">Secondary</button>
    <button type="button" class="btn btn-success">Success</button>
    <button type="button" class="btn btn-warning">Warning</button>
    <button type="button" class="btn btn-danger">Danger</button>
    <button type="button" class="btn btn-info">Info</button>
    <button type="button" class="btn btn-ghost">Ghost</button>
</div>

Outline & size variants

3. Buttons
<div class="audit-row"><button type="button" class="btn btn-outline">Outline</button>
    <button type="button" class="btn btn-primary btn-sm">Small primary</button>
    <button type="button" class="btn btn-primary btn-lg">Large primary</button>
    <button type="button" class="btn btn-primary" disabled="">Disabled primary</button>
    <button type="button" class="btn btn-secondary" disabled="">Disabled secondary</button>
</div>

Buttons with icons

3. Buttons
<div class="audit-row"><button type="button" class="btn btn-primary"><span data-icon="check" data-icon-size="16"></span> Approve</button>
    <button type="button" class="btn btn-danger"><span data-icon="trash" data-icon-size="16"></span> Delete</button>
    <button type="button" class="btn btn-info"><span data-icon="info" data-icon-size="16"></span> Details</button>
</div>

4. Forms

Bare HTML inputs (.form-input)

4. Forms


<div class="audit-form-grid"><div>
        <label for="in-text">Customer name</label>
        <input id="in-text" type="text" class="form-input" value="Alice Bloggs">
    </div>
    <div>
        <label for="in-email">Email address</label>
        <input id="in-email" type="email" class="form-input" value="alice@example.com">
    </div>
    <div>
        <label for="in-pass">Password</label>
        <input id="in-pass" type="password" class="form-input" value="hunter2hunter2">
    </div>
    <div>
        <label for="in-num">Order quantity</label>
        <input id="in-num" type="number" class="form-input" value="42">
    </div>
    <div>
        <label for="in-date">Invoice date</label>
        <input id="in-date" type="date" class="form-input" value="2026-04-22">
    </div>
    <div>
        <label for="in-select">Status</label>
        <select id="in-select" class="form-input">
            <option>Draft</option>
            <option selected="">In review</option>
            <option>Approved</option>
            <option>Rejected</option>
        </select>
    </div>
    <div style="grid-column: 1 / -1;">
        <label for="in-textarea">Notes</label>
        <textarea id="in-textarea" class="form-input" rows="3">Customer requested next-day delivery; left a note to wrap securely.</textarea>
    </div>
    <div>
        <label><input type="checkbox" checked=""> Subscribe to weekly digest</label><br>
        <label><input type="checkbox"> Receive marketing emails</label>
    </div>
    <div>
        <label><input type="radio" name="r-priority" checked=""> Standard priority</label><br>
        <label><input type="radio" name="r-priority"> Express priority</label>
    </div>
    <div>
        <label for="in-file">Upload signed PDF</label>
        <input id="in-file" type="file" class="form-input">
    </div>
</div>

F.create() - blueprint-driven form

4. Forms
<div id="audit-form-host"></div>
/**
 * 4. Forms - from public/showcase/themes.
 */
export function mount(root) {
      // F.create() - schema, data, options β†’ renderTo(selector)
    if (window.F && F.create) {
        try {
          const blueprint = {
            name:    { type: M.types.string, required: true, label: 'Customer name' },
            email:   { type: M.types.string, required: true, label: 'Email', formConfig: { inputType: 'email' } },
            country: { type: M.types.string, label: 'Country', formConfig: { component: 'select', options: ['United Kingdom', 'United States', 'France', 'Germany'] } },
            notes:   { type: M.types.string, label: 'Notes', formConfig: { component: 'textarea' } }
          };
          F.create(blueprint, {}, { layout: 'grid', columns: 2, submitLabel: 'Save customer' }).renderTo('#audit-form-host');
        } catch (err) {
          $(root).find('#audit-form-host').append(
            $('<div class="alert alert-warning"></div>').text(
              'F.create() blueprint demo unavailable: ' + err.message
            )
          );
        }
      }
}

5. Cards - header / footer combinations

Four standard combinations

5. Cards - header / footer combinations

Body only. No header, no footer. Just a single padded surface.

Header only

Header above, body below. No footer.

Body with a footer beneath. No header.

Header and footer

Full sandwich - header, body, and footer.

<div class="audit-grid"><div class="card">
        <div class="card-body">
            <p class="card-text"><strong>Body only.</strong> No header, no footer. Just a single padded surface.</p>
        </div>
    </div>

    <div class="card">
        <div class="card-header"><h4 class="card-title">Header only</h4></div>
        <div class="card-body">
            <p class="card-text">Header above, body below. No footer.</p>
        </div>
    </div>

    <div class="card">
        <div class="card-body">
            <p class="card-text">Body with a footer beneath. No header.</p>
        </div>
        <div class="card-footer">Footer content</div>
    </div>

    <div class="card">
        <div class="card-header"><h4 class="card-title">Header and footer</h4></div>
        <div class="card-body">
            <p class="card-text">Full sandwich - header, body, and footer.</p>
        </div>
        <div class="card-footer">Footer content</div>
    </div>
</div>

Hover & primary variants

5. Cards - header / footer combinations

Hover card

Hover the card to see the elevation effect adjust to the active theme.

card-primary

Solid primary-tinted card. Watch this one carefully on light themes - header text often breaks here.

<div class="audit-grid"><div class="card card-hover">
        <div class="card-header"><h4 class="card-title">Hover card</h4></div>
        <div class="card-body"><p class="card-text">Hover the card to see the elevation effect adjust to the active theme.</p></div>
    </div>
    <div class="card card-primary">
        <div class="card-header"><h4 class="card-title">card-primary</h4></div>
        <div class="card-body"><p class="card-text">Solid primary-tinted card. Watch this one carefully on light themes - header text often breaks here.</p></div>
        <div class="card-footer">Footer content</div>
    </div>
</div>

Accent cards (left-border colour cue + label)

5. Cards - header / footer combinations

Primary card-accent

Default accent - uses --dm-primary for the left border.

Success card-accent-success

Tinted accent with a green left border. Use for confirmation and completion states.

Warning card-accent-warning

Yellow accent. Lemon-light theme has historically broken this combination.

Danger card-accent-danger

Red accent. Used for destructive confirmation flows.

Info card-accent-info

Blue accent. Mint-light historically had cyan-on-mint contrast issues here.

Collapsible card

Click the header to toggle. State persists to localStorage.

<div class="audit-grid"><div class="card card-accent">
        <div class="card-header"><h4 class="card-title"><span class="accent-tag primary">Primary</span> card-accent</h4></div>
        <div class="card-body"><p class="card-text">Default accent - uses <code>--dm-primary</code> for the left border.</p></div>
    </div>
    <div class="card card-accent card-accent-success">
        <div class="card-header"><h4 class="card-title"><span class="accent-tag success">Success</span> card-accent-success</h4></div>
        <div class="card-body"><p class="card-text">Tinted accent with a green left border. Use for confirmation and completion states.</p></div>
    </div>
    <div class="card card-accent card-accent-warning">
        <div class="card-header"><h4 class="card-title"><span class="accent-tag warning">Warning</span> card-accent-warning</h4></div>
        <div class="card-body"><p class="card-text">Yellow accent. Lemon-light theme has historically broken this combination.</p></div>
    </div>
    <div class="card card-accent card-accent-danger">
        <div class="card-header"><h4 class="card-title"><span class="accent-tag danger">Danger</span> card-accent-danger</h4></div>
        <div class="card-body"><p class="card-text">Red accent. Used for destructive confirmation flows.</p></div>
    </div>
    <div class="card card-accent card-accent-info">
        <div class="card-header"><h4 class="card-title"><span class="accent-tag info">Info</span> card-accent-info</h4></div>
        <div class="card-body"><p class="card-text">Blue accent. Mint-light historically had cyan-on-mint contrast issues here.</p></div>
    </div>
    <div class="card card-collapsible" id="audit-card-collapsible">
        <div class="card-header"><h4 class="card-title">Collapsible card</h4></div>
        <div class="card-body"><p class="card-text">Click the header to toggle. State persists to localStorage.</p></div>
    </div>
</div>
/**
 * 5. Cards - header / footer combinations - from public/showcase/themes.
 */
export function mount(root) {
    if (E.card) E.card(root.querySelector('#audit-card-collapsible'), { collapsible: true });
}

6. Badges, Pills & Number Badges

Badges

Primary

Secondary

Success

Warning

Danger

Info

Pills

Primary pill

Secondary pill

Light pill

Outline pill

Outline primary

Small

Large

Number badges (notification counters)

6. Badges, Pills & Number Badges
<div class="audit-row"><button type="button" class="btn btn-primary" id="numbadge-host-1" style="position:relative;">Inbox</button>
    <button type="button" class="btn btn-secondary" id="numbadge-host-2" style="position:relative;">Tasks</button>
    <button type="button" class="btn btn-danger" id="numbadge-host-3" style="position:relative;">Alerts</button>
</div>
/**
 * 6. Badges, Pills & Number Badges - from public/showcase/themes.
 */
export function mount(root) {
    if (E.numberBadge) {
      E.numberBadge(root.querySelector('#numbadge-host-1'), { count: 12 });
      E.numberBadge(root.querySelector('#numbadge-host-2'), { count: 4 });
      E.numberBadge(root.querySelector('#numbadge-host-3'), { count: 99, max: 99 });
    }
}

7. Alerts

Primary alert. System maintenance scheduled for 02:00-04:00 on Sunday.

Success. All overnight CI jobs completed without errors.

Warning. Disk usage on prod-db-01 is at 85% - consider archiving older logs.

Danger. Webhook delivery to Stripe failed three consecutive times.

Info. A new release of Domma is available - see the changelog.

8. Tabs & Accordion

8. Tabs & Accordion

Tab content one - body text rendered inside the active panel. Headings, links and emphasised text should remain legible across all themes.

Tab content two - secondary tab. Inline link visibility check.

Tab content three - settings tab content.

Accordion body text. The chevron icon and the header background should both be readable.

Body of the second section.

<div id="audit-tabs" class="tabs">
    <div class="tab-list" role="tablist">
        <button type="button" class="tab-item active" data-tab="0" role="tab">Overview</button>
        <button type="button" class="tab-item" data-tab="1" role="tab">Activity</button>
        <button type="button" class="tab-item" data-tab="2" role="tab">Settings</button>
    </div>
    <div class="tab-content">
        <div class="tab-panel active" role="tabpanel"><p>Tab content one - body text rendered inside the active panel. Headings, links and emphasised text should remain legible across all themes.</p></div>
        <div class="tab-panel" role="tabpanel"><p>Tab content two - secondary tab. <a href="#">Inline link</a> visibility check.</p></div>
        <div class="tab-panel" role="tabpanel"><p>Tab content three - settings tab content.</p></div>
    </div>
</div>
<div id="audit-accordion" class="accordion" style="margin-top: 1rem;">
    <div class="accordion-item">
        <button class="accordion-header" type="button">First section header</button>
        <div class="accordion-body"><p>Accordion body text. The chevron icon and the header background should both be readable.</p></div>
    </div>
    <div class="accordion-item">
        <button class="accordion-header" type="button">Second section header</button>
        <div class="accordion-body"><p>Body of the second section.</p></div>
    </div>
</div>
/**
 * 8. Tabs & Accordion - from public/showcase/themes.
 */
export function mount(root) {
      // ── Initialise every Domma component ─────────────────────────────────
    E.tabs(root.querySelector('#audit-tabs'));
    E.accordion(root.querySelector('#audit-accordion'));
}

9. Tables

Bare HTML table (.table)

ID Customer Status Total
INV-0001 Acme Ltd Paid Β£4,250.00
INV-0002 Globex Pending Β£1,180.00
INV-0003 Initech Overdue Β£760.00
INV-0004 Soylent Corp Paid Β£12,500.00

T.create() - sortable, searchable, paginated

9. Tables
<div id="audit-table-host"></div>
/**
 * 9. Tables - from public/showcase/themes.
 */
export function mount(root) {
    if (window.T && T.create) {
      const data = [
        { id: 'INV-0001', customer: 'Acme Ltd',     status: 'Paid',    total: 4250.00 },
        { id: 'INV-0002', customer: 'Globex',       status: 'Pending', total: 1180.00 },
        { id: 'INV-0003', customer: 'Initech',      status: 'Overdue', total:  760.00 },
        { id: 'INV-0004', customer: 'Soylent Corp', status: 'Paid',    total: 12500.00 },
        { id: 'INV-0005', customer: 'Stark Inds.',  status: 'Paid',    total: 8700.50 },
        { id: 'INV-0006', customer: 'Wayne Ent.',   status: 'Pending', total: 3200.00 },
        { id: 'INV-0007', customer: 'Oscorp',       status: 'Overdue', total: 1900.00 },
        { id: 'INV-0008', customer: 'Tyrell Corp',  status: 'Paid',    total: 5450.75 }
      ];
      T.create('#audit-table-host', {
        data,
        columns: [
          { key: 'id', label: 'Invoice' },
          { key: 'customer', label: 'Customer' },
          { key: 'status', label: 'Status' },
          { key: 'total', label: 'Total', format: v => 'Β£' + v.toLocaleString('en-GB', { minimumFractionDigits: 2 }) }
        ],
        pagination: true,
        pageSize: 5,
        striped: true
      });
    }
}

10. Lists & List Groups

10. Lists & List Groups
Standard lists
  • Onboarding session with the new client
  • Migrate legacy invoices to the new system
  • Review Q2 procurement reports
  1. Stage the migration
  2. Run the dry-run script
  3. Cut over and verify
API key
A unique identifier used to authenticate requests.
Rate limit
1,000 requests per hour, per key.
List group with colour variants
Q1 sales report
Selected item
Primary tinted
Success tinted
Warning tinted
Danger tinted
Info tinted
Disabled item
<div class="row"><div class="col-6">
        <div class="audit-label">Standard lists</div>
        <ul>
            <li>Onboarding session with the new client</li>
            <li>Migrate legacy invoices to the new system</li>
            <li>Review Q2 procurement reports</li>
        </ul>
        <ol>
            <li>Stage the migration</li>
            <li>Run the dry-run script</li>
            <li>Cut over and verify</li>
        </ol>
        <dl>
            <dt>API key</dt>
            <dd>A unique identifier used to authenticate requests.</dd>
            <dt>Rate limit</dt>
            <dd>1,000 requests per hour, per key.</dd>
        </dl>
    </div>
    <div class="col-6">
        <div class="audit-label">List group with colour variants</div>
        <div id="audit-listgroup" class="list-group">
            <div class="list-group-item list-group-item-action">Q1 sales report</div>
            <div class="list-group-item list-group-item-action active">Selected item</div>
            <div class="list-group-item list-group-item-action list-group-item-primary">Primary tinted</div>
            <div class="list-group-item list-group-item-action list-group-item-success">Success tinted</div>
            <div class="list-group-item list-group-item-action list-group-item-warning">Warning tinted</div>
            <div class="list-group-item list-group-item-action list-group-item-danger">Danger tinted</div>
            <div class="list-group-item list-group-item-action list-group-item-info">Info tinted</div>
            <div class="list-group-item list-group-item-action disabled">Disabled item</div>
        </div>
    </div>
</div>
/**
 * 10. Lists & List Groups - from public/showcase/themes.
 */
export function mount(root) {
    E.listGroup(root.querySelector('#audit-listgroup'), { selectable: true, multiSelect: false });
}

11. Navigation Components

Breadcrumbs

11. Navigation Components
<div id="audit-breadcrumbs"></div>
/**
 * 11. Navigation Components - from public/showcase/themes.
 */
export function mount(root) {
    E.breadcrumbs(root.querySelector('#audit-breadcrumbs'), {
      items: [
        { text: 'Home', url: '#' },
        { text: 'Showcase', url: '#' },
        { text: 'Themes', active: true }
      ],
      separator: 'chevron'
    });
}

Pagination

β€Ή Prev

1

2

3

4

…

12

Next β€Ί

Button group - single-select

11. Navigation Components
<div id="audit-bg-single" class="btn-group" role="group">
    <button type="button" class="btn btn-outline active" data-value="d">Daily</button>
    <button type="button" class="btn btn-outline" data-value="w">Weekly</button>
    <button type="button" class="btn btn-outline" data-value="m">Monthly</button>
    <button type="button" class="btn btn-outline" data-value="q">Quarterly</button>
</div>
/**
 * 11. Navigation Components - from public/showcase/themes.
 */
export function mount(root) {
    E.buttonGroup(root.querySelector('#audit-bg-single'), { mode: 'single' });
}

Dropdown

11. Navigation Components
<div id="audit-dropdown" class="dropdown" style="display:inline-block;">
    <button type="button" class="btn btn-secondary dropdown-toggle">Actions β–Ύ</button>
    <div class="dropdown-menu">
        <a href="#" class="dropdown-item">Edit record</a>
        <a href="#" class="dropdown-item">Duplicate</a>
        <a href="#" class="dropdown-item">Archive</a>
        <hr>
        <a href="#" class="dropdown-item text-danger">Delete permanently</a>
    </div>
</div>
/**
 * 11. Navigation Components - from public/showcase/themes.
 */
export function mount(root) {
    E.dropdown(root.querySelector('#audit-dropdown'));
}

12. Loaders & Progress

12. Loaders & Progress
Spinner
Dots
Pulse
Bars
<div class="audit-grid"><div class="audit-block"><div class="audit-label">Spinner</div><div id="loader-spin" style="height:48px;"></div></div>
    <div class="audit-block"><div class="audit-label">Dots</div><div id="loader-dots" style="height:48px;"></div></div>
    <div class="audit-block"><div class="audit-label">Pulse</div><div id="loader-pulse" style="height:48px;"></div></div>
    <div class="audit-block"><div class="audit-label">Bars</div><div id="loader-bars" style="height:48px;"></div></div>
</div>
/**
 * 12. Loaders & Progress - from public/showcase/themes.
 */
export function mount(root) {
    E.loader(root.querySelector('#loader-spin'),  { type: 'spinner', size: 'medium' }).show();
    E.loader(root.querySelector('#loader-dots'),  { type: 'dots',    size: 'medium' }).show();
    E.loader(root.querySelector('#loader-pulse'), { type: 'pulse',   size: 'medium' }).show();
    E.loader(root.querySelector('#loader-bars'),  { type: 'bars',    size: 'medium' }).show();
}

Progress bars

12. Loaders & Progress
32%
Complete
67%
12%
<div class="progress-bar" style="width: 32%;">32%</div>
<div class="progress-bar progress-bar-success" style="width: 100%;">Complete</div>
<div class="progress-bar progress-bar-warning" style="width: 67%;">67%</div>
<div class="progress-bar progress-bar-danger" style="width: 12%;">12%</div>

13. Tooltips

13. Tooltips
<div class="audit-row"><button type="button" class="btn btn-primary" data-tooltip="Tooltip on top" data-tooltip-position="top">Hover (top)</button>
    <button type="button" class="btn btn-secondary" data-tooltip="Tooltip on bottom" data-tooltip-position="bottom">Hover (bottom)</button>
    <button type="button" class="btn btn-info" data-tooltip="Tooltip on left" data-tooltip-position="left">Hover (left)</button>
    <button type="button" class="btn btn-warning" data-tooltip="Tooltip on right" data-tooltip-position="right">Hover (right)</button>
</div>

14. Modals, Dialogs & Toasts

Trigger buttons

14. Modals, Dialogs & Toasts
<div class="audit-row"><button type="button" class="btn btn-primary" id="audit-open-modal">Open modal</button>
    <button type="button" class="btn btn-info" id="audit-open-alert">Show alert dialog</button>
    <button type="button" class="btn btn-warning" id="audit-open-confirm">Show confirm dialog</button>
    <button type="button" class="btn btn-secondary" id="audit-open-prompt">Show prompt dialog</button>
    <button type="button" class="btn btn-success" id="audit-toast-success">Toast: success</button>
    <button type="button" class="btn btn-danger" id="audit-toast-error">Toast: error</button>
</div>
/**
 * 14. Modals, Dialogs & Toasts - from public/showcase/themes.
 */
export function mount(root) {
      // Modal trigger
    function buildPopupModal() {
        const m = document.createElement('div');
        m.id = 'audit-popup-modal';
        m.className = 'modal';

        const header = document.createElement('div');
        header.className = 'modal-header';
        const title = document.createElement('h5');
        title.className = 'modal-title';
        title.textContent = 'Modal popup';
        const closeX = document.createElement('button');
        closeX.className = 'modal-close';
        closeX.setAttribute('aria-label', 'Close');
        closeX.textContent = 'Γ—';
        header.appendChild(title);
        header.appendChild(closeX);

        const body = document.createElement('div');
        body.className = 'modal-body';
        const p = document.createElement('p');
        p.textContent = 'This is a popup modal. Inspect the header, body, footer and close-button contrast.';
        body.appendChild(p);

        const footer = document.createElement('div');
        footer.className = 'modal-footer';
        const btnCancel = document.createElement('button');
        btnCancel.className = 'btn btn-secondary modal-close';
        btnCancel.textContent = 'Cancel';
        const btnConfirm = document.createElement('button');
        btnConfirm.className = 'btn btn-primary modal-close';
        btnConfirm.textContent = 'Confirm';
        footer.appendChild(btnCancel);
        footer.appendChild(btnConfirm);

        m.appendChild(header);
        m.appendChild(body);
        m.appendChild(footer);
        document.body.appendChild(m);
        return m;
      }
    $(root).find('#audit-open-modal').on('click', () => {
        if (!$(root).find('#audit-popup-modal').length) buildPopupModal();
        E.modal(root.querySelector('#audit-popup-modal')).open();
      });
    $(root).find('#audit-open-alert').on('click', () => {
        E.alert('Alert dialog content', { title: 'Notice' });
      });
    $(root).find('#audit-open-confirm').on('click', () => {
        E.confirm('Are you sure you want to proceed with this action?', { title: 'Confirm action' });
      });
    $(root).find('#audit-open-prompt').on('click', () => {
        E.prompt('Enter your name:', { title: 'Identify yourself', inputValue: 'Darryl' });
      });
    $(root).find('#audit-toast-success').on('click', () => {
        E.toast('Operation completed successfully', { type: 'success' });
      });
    $(root).find('#audit-toast-error').on('click', () => {
        E.toast('Something went wrong - please try again', { type: 'error' });
      });
}

Inline modal (forced visible for inspection)

14. Modals, Dialogs & Toasts
<div class="modal-header"><h5 class="modal-title">Confirm payment</h5></div>
<div class="modal-body">
    <p>Are you sure you want to process the payment of <strong>Β£4,250.00</strong> to <strong>Acme Ltd</strong>?</p>
    <p class="text-secondary">A receipt will be emailed to the registered billing address.</p>
</div>
<div class="modal-footer">
    <button type="button" class="btn btn-secondary">Cancel</button>
    <button type="button" class="btn btn-primary">Confirm payment</button>
</div>

15. Slideovers

15. Slideovers
<div class="audit-row"><button type="button" class="btn btn-primary" id="slideover-left">From left</button>
    <button type="button" class="btn btn-primary" id="slideover-right">From right</button>
    <button type="button" class="btn btn-primary" id="slideover-top">From top</button>
    <button type="button" class="btn btn-primary" id="slideover-bottom">From bottom</button>
</div>
/**
 * 15. Slideovers - from public/showcase/themes.
 */
export function mount(root) {
    function openSlideover(position) {
        const id = 'audit-slideover-' + position;
        let host = $('#' + id)[0];
        if (!host) {
          host = document.createElement('div');
          host.id = id;
          document.body.appendChild(host);
        }
        E.slideover('#' + id, {
          position,
          title: 'Slideover - ' + position,
          content: 'This is the slideover panel. Inspect heading colour, body text and the close-button contrast.',
          backdrop: true,
          backdropClose: true
        }).open();
      }
    $(root).find('#slideover-left').on('click', () => openSlideover('left'));
    $(root).find('#slideover-right').on('click', () => openSlideover('right'));
    $(root).find('#slideover-top').on('click', () => openSlideover('top'));
    $(root).find('#slideover-bottom').on('click', () => openSlideover('bottom'));
}

16. Autocomplete & Pillbox

16. Autocomplete & Pillbox
Autocomplete
Pillbox (multi-select)
<div class="row"><div class="col-6">
        <div class="audit-label">Autocomplete</div>
        <input id="audit-autocomplete" class="form-input" placeholder="Type a country…">
    </div>
    <div class="col-6">
        <div class="audit-label">Pillbox (multi-select)</div>
        <input type="text" id="audit-pillbox" class="form-input" placeholder="Add tags…">
    </div>
</div>
/**
 * 16. Autocomplete & Pillbox - from public/showcase/themes.
 */
export function mount(root) {
    E.autocomplete(root.querySelector('#audit-autocomplete'), {
        data: ['United Kingdom', 'United States', 'France', 'Germany', 'Spain', 'Italy', 'Japan', 'Australia', 'Brazil', 'Canada', 'Norway', 'Sweden', 'Denmark', 'Finland', 'Netherlands'],
        minChars: 1,
        maxResults: 8
      });
    E.pillbox(root.querySelector('#audit-pillbox'), {
        data: ['JavaScript', 'TypeScript', 'CSS', 'HTML', 'Domma', 'Node.js', 'PHP', 'Python', 'Rust', 'Go'],
        value: ['JavaScript', 'Domma'],
        creatable: true,
        placeholder: 'Add a tag…'
      });
}

17. Progression - Timeline & Roadmap

Timeline mode

17. Progression - Timeline & Roadmap
<div id="audit-timeline"></div>
/**
 * 17. Progression - Timeline & Roadmap - from public/showcase/themes.
 */
export function mount(root) {
    E.progression(root.querySelector('#audit-timeline'), {
      mode: 'timeline',
      items: [
        { year: 2023, title: 'Founded', description: 'Company established and first round of seed funding raised.' },
        { year: 2024, title: 'Series A', description: 'Closed a Β£10m Series A led by Acme Ventures.' },
        { year: 2025, title: 'Expansion', description: 'Opened offices in Berlin, Paris, and Amsterdam.' },
        { year: 2026, title: 'Public release', description: 'Domma 1.0 ships to general availability.' }
      ]
    });
}

Roadmap mode (status colours)

17. Progression - Timeline & Roadmap
<div id="audit-roadmap"></div>
/**
 * 17. Progression - Timeline & Roadmap - from public/showcase/themes.
 */
export function mount(root) {
    E.progression(root.querySelector('#audit-roadmap'), {
      mode: 'roadmap',
      items: [
        { id: 1, title: 'API v2',     status: 'completed',   description: 'New REST endpoints with pagination.' },
        { id: 2, title: 'Mobile app', status: 'in-progress', description: 'iOS and Android clients in beta.' },
        { id: 3, title: 'Analytics',  status: 'planned',     description: 'Self-service reporting dashboard.' },
        { id: 4, title: 'Compliance', status: 'blocked',     description: 'Awaiting legal review.' }
      ]
    });
}

18. Carousel

18. Carousel
<div id="audit-carousel" class="carousel" style="max-width: 600px; margin: 0 auto;">
    <div class="carousel-inner">
        <div class="carousel-item active">
            <div style="background: var(--dm-primary); color: var(--dm-primary-text, #fff); padding: 3rem 1rem; text-align:center; border-radius:8px;">
                <h3 style="color: inherit; margin: 0;">Slide one - primary background</h3>
                <p style="color: inherit; margin: 0.5rem 0 0;">Caption text rendered against the primary brand colour.</p>
            </div>
        </div>
        <div class="carousel-item">
            <div style="background: var(--dm-success); color: var(--dm-success-text, #fff); padding: 3rem 1rem; text-align:center; border-radius:8px;">
                <h3 style="color: inherit; margin: 0;">Slide two - success background</h3>
                <p style="color: inherit; margin: 0.5rem 0 0;">Same caption pattern using success colour.</p>
            </div>
        </div>
        <div class="carousel-item">
            <div style="background: var(--dm-warning); color: var(--dm-warning-text, #fff); padding: 3rem 1rem; text-align:center; border-radius:8px;">
                <h3 style="color: inherit; margin: 0;">Slide three - warning background</h3>
                <p style="color: inherit; margin: 0.5rem 0 0;">Common contrast hot-spot in light themes.</p>
            </div>
        </div>
    </div>
</div>
/**
 * 18. Carousel - from public/showcase/themes.
 */
export function mount(root) {
    E.carousel(root.querySelector('#audit-carousel'), { autoplay: true, interval: 5000, pauseOnHover: true });
}

19. Code & quotes

Inline code: const x = Domma.theme.set('mint-light') should remain readable.

function audit() {
  const issues = [];
  document.querySelectorAll('*').forEach(el => {
    if (failsContrast(el)) issues.push(el);
  });
  return issues;
}

Usage

Usage

CSS Only

<!-- Include themes CSS --> <link rel="stylesheet" href="domma-themes.css"> <!-- Apply via class --> <body class="dm-theme-grayve-dark">

JavaScript API

// Initialise with a theme Domma.theme.init({ theme: 'grayve-dark', autoDetect: true }); // Switch at runtime Domma.theme.set('ocean-light'); // Query current state Domma.theme.get(); // 'grayve-dark' Domma.theme.getBase(); // 'grayve' Domma.theme.isDark(); // true
<div class="themes-component-grid"><div class="themes-preview-card">
        <h3>CSS Only</h3>
        <div class="themes-code-block language-html">
            <span class="comment">&lt;!-- Include themes CSS --&gt;</span>
            &lt;<span class="keyword">link</span> <span class="function">rel</span>=<span class="string">"stylesheet"</span> <span class="function">href</span>=<span class="string">"domma-themes.css"</span>&gt;

            <span class="comment">&lt;!-- Apply via class --&gt;</span>
            &lt;<span class="keyword">body</span> <span class="function">class</span>=<span class="string">"dm-theme-grayve-dark"</span>&gt;
        </div>
    </div>

    <div class="themes-preview-card">
        <h3>JavaScript API</h3>
        <div class="themes-code-block language-javascript">
            <span class="comment">// Initialise with a theme</span>
            Domma.theme.<span class="function">init</span>({
            theme: <span class="string">'grayve-dark'</span>,
            autoDetect: <span class="keyword">true</span>
            });

            <span class="comment">// Switch at runtime</span>
            Domma.theme.<span class="function">set</span>(<span class="string">'ocean-light'</span>);

            <span class="comment">// Query current state</span>
            Domma.theme.<span class="function">get</span>();      <span class="comment">// 'grayve-dark'</span>
            Domma.theme.<span class="function">getBase</span>(); <span class="comment">// 'grayve'</span>
            Domma.theme.<span class="function">isDark</span>(); <span class="comment">// true</span>
        </div>
    </div>
</div>