Skip to content

Dark Mode

Inertia Table uses Tailwind's dark: variant for all styling. Every default class has a light and dark counterpart - backgrounds, text colours, borders, hover states, badge variants, and sort indicators all adapt automatically. No configuration is required beyond having dark mode working in your Tailwind setup.

How It Works

The package uses the dark: class variant throughout. When the dark class is present on your <html> element (or whichever element your Tailwind config targets), the dark styles activate.

Default Light vs Dark

ElementLightDark
Wrapperbg-white border-gray-200bg-gray-900 border-gray-700
Headerbg-gray-50 text-gray-600bg-gray-800 text-gray-400
Rowstext-gray-900 hover:bg-gray-50text-gray-200 hover:bg-gray-800
Bordersborder-gray-200 divide-gray-100border-gray-700 divide-gray-700
Searchbg-white border-gray-300bg-gray-800 border-gray-600
Paginationborder-gray-300 text-gray-500border-gray-600 text-gray-400
BadgesLight tinted backgroundsDark tinted backgrounds

Tailwind Configuration

Tailwind v4

Tailwind v4 uses a CSS-based dark mode by default. If you're using the class strategy, add the custom variant to your CSS:

css
/* resources/css/app.css */
@custom-variant dark (&:where(.dark, .dark *));

Tailwind v3

In tailwind.config.js, set the dark mode strategy:

js
module.exports = {
    darkMode: 'class',
    // ...
}

The class strategy is recommended because it gives you programmatic control over the theme. The media strategy (which follows the OS preference) also works but can't be toggled by users.

Toggling Dark Mode

The table itself doesn't manage dark mode state - it reacts to the dark class on your document. Here's a common pattern for toggling between system, light, and dark modes:

tsx
import { useState, useEffect } from 'react';

type ThemeMode = 'system' | 'light' | 'dark';

function applyTheme(mode: ThemeMode) {
    const root = document.documentElement;
    if (mode === 'dark') {
        root.classList.add('dark');
    } else if (mode === 'light') {
        root.classList.remove('dark');
    } else {
        // System preference
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
            root.classList.add('dark');
        } else {
            root.classList.remove('dark');
        }
    }
}

function useTheme() {
    const [mode, setMode] = useState<ThemeMode>(() => {
        if (typeof window === 'undefined') return 'system';
        return (localStorage.getItem('theme') as ThemeMode) || 'system';
    });

    useEffect(() => {
        applyTheme(mode);
        localStorage.setItem('theme', mode);

        if (mode === 'system') {
            const mq = window.matchMedia('(prefers-color-scheme: dark)');
            const handler = () => applyTheme('system');
            mq.addEventListener('change', handler);
            return () => mq.removeEventListener('change', handler);
        }
    }, [mode]);

    return { mode, setMode };
}
vue
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';

type ThemeMode = 'system' | 'light' | 'dark';

function applyTheme(mode: ThemeMode) {
    const root = document.documentElement;
    if (mode === 'dark') {
        root.classList.add('dark');
    } else if (mode === 'light') {
        root.classList.remove('dark');
    } else {
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
            root.classList.add('dark');
        } else {
            root.classList.remove('dark');
        }
    }
}

const mode = ref<ThemeMode>(
    (localStorage.getItem('theme') as ThemeMode) || 'system'
);

watch(mode, (newMode) => {
    applyTheme(newMode);
    localStorage.setItem('theme', newMode);
});

onMounted(() => {
    applyTheme(mode.value);

    if (mode.value === 'system') {
        const mq = window.matchMedia('(prefers-color-scheme: dark)');
        mq.addEventListener('change', () => applyTheme('system'));
    }
});
</script>

Overriding Dark Styles

Since all styling is done via Tailwind classes, you can override any dark mode colour using the classNames prop. Overrides replace the entire class string for that element, so include both light and dark variants.

Full Theme Override

tsx
<InertiaTable
    tableData={servers}
    classNames={{
        wrapper: 'rounded-xl border border-slate-200 bg-white shadow dark:border-slate-700 dark:bg-slate-900',
        thead: 'bg-slate-50 border-b border-slate-200 dark:bg-slate-800 dark:border-slate-700',
        th: 'px-4 py-3 text-left text-xs font-semibold text-slate-600 uppercase dark:text-slate-400',
        tbody: 'divide-y divide-slate-100 dark:divide-slate-700',
        tr: 'hover:bg-slate-50 dark:hover:bg-slate-800',
        td: 'px-4 py-3 text-sm text-slate-900 dark:text-slate-200',
    }}
/>
template
<InertiaTable
    :table-data="servers"
    :class-names="{
        wrapper: 'rounded-xl border border-slate-200 bg-white shadow dark:border-slate-700 dark:bg-slate-900',
        thead: 'bg-slate-50 border-b border-slate-200 dark:bg-slate-800 dark:border-slate-700',
        th: 'px-4 py-3 text-left text-xs font-semibold text-slate-600 uppercase dark:text-slate-400',
        tbody: 'divide-y divide-slate-100 dark:divide-slate-700',
        tr: 'hover:bg-slate-50 dark:hover:bg-slate-800',
        td: 'px-4 py-3 text-sm text-slate-900 dark:text-slate-200',
    }"
/>

Dark-Only Adjustments

If you only need to tweak the dark variant while keeping the light defaults, override the specific key and rewrite both sides:

tsx
<InertiaTable
    tableData={servers}
    classNames={{
        // Keep the light default, change dark background to zinc
        wrapper: 'rounded-lg border border-gray-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900',
        tr: 'hover:bg-gray-50 transition-colors dark:hover:bg-zinc-800',
    }}
/>
template
<InertiaTable
    :table-data="servers"
    :class-names="{
        wrapper: 'rounded-lg border border-gray-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900',
        tr: 'hover:bg-gray-50 transition-colors dark:hover:bg-zinc-800',
    }"
/>

Branded Dark Theme

tsx
// A purple-tinted dark theme
const brandedClassNames = {
    wrapper: 'rounded-lg border border-gray-200 bg-white shadow-sm dark:border-purple-900 dark:bg-gray-950',
    thead: 'bg-gray-50 border-b border-gray-200 dark:bg-purple-950/50 dark:border-purple-900',
    th: 'px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase dark:text-purple-300',
    tr: 'hover:bg-gray-50 transition-colors dark:hover:bg-purple-950/30',
    td: 'px-4 py-3 text-sm text-gray-900 dark:text-purple-100',
    search: 'rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-purple-500 focus:ring-purple-500 dark:border-purple-800 dark:bg-gray-900 dark:text-purple-100',
};

<InertiaTable tableData={servers} classNames={brandedClassNames} />
vue
<script setup lang="ts">
// A purple-tinted dark theme
const brandedClassNames = {
    wrapper: 'rounded-lg border border-gray-200 bg-white shadow-sm dark:border-purple-900 dark:bg-gray-950',
    thead: 'bg-gray-50 border-b border-gray-200 dark:bg-purple-950/50 dark:border-purple-900',
    th: 'px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase dark:text-purple-300',
    tr: 'hover:bg-gray-50 transition-colors dark:hover:bg-purple-950/30',
    td: 'px-4 py-3 text-sm text-gray-900 dark:text-purple-100',
    search: 'rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-purple-500 focus:ring-purple-500 dark:border-purple-800 dark:bg-gray-900 dark:text-purple-100',
};
</script>

<template>
    <InertiaTable :table-data="servers" :class-names="brandedClassNames" />
</template>

Row-Level Dark Styling

Use rowClassName for conditional dark mode styling per row:

tsx
<InertiaTable
    tableData={servers}
    rowClassName={(row) => {
        if (row.status === 'failed') return 'bg-red-50 dark:bg-red-950/30';
        if (row.status === 'pending') return 'bg-yellow-50 dark:bg-yellow-950/20';
        return '';
    }}
/>
template
<InertiaTable
    :table-data="servers"
    :row-class-name="(row) => {
        if (row.status === 'failed') return 'bg-red-50 dark:bg-red-950/30';
        if (row.status === 'pending') return 'bg-yellow-50 dark:bg-yellow-950/20';
        return '';
    }"
/>

Badge Colours in Dark Mode

Badge variants automatically switch between light and dark palettes:

VariantLightDark
successbg-green-100 text-green-800bg-green-900 text-green-200
dangerbg-red-100 text-red-800bg-red-900 text-red-200
warningbg-yellow-100 text-yellow-800bg-yellow-900 text-yellow-200
infobg-blue-100 text-blue-800bg-blue-900 text-blue-200
defaultbg-gray-100 text-gray-800bg-gray-700 text-gray-200
graybg-gray-100 text-gray-700bg-gray-700 text-gray-300
destructivebg-red-100 text-red-800bg-red-900 text-red-200
outlineborder-gray-300 text-gray-700border-gray-600 text-gray-300

These are built into the package. To customise badge colours, use a cellRenderers override (React) or a #cell-{columnName} slot (Vue) on the specific column - see Overriding Cells.

CSS Variables (shadcn)

If you're using shadcn/ui with CSS variable-based theming, the table's default Tailwind classes won't automatically match your theme tokens. Use the classNames prop to map to your CSS variables, or use the useTable hook with shadcn components directly - see shadcn/ui Integration.