Skip to content

Dark Mode ​

DocBits components support full dark mode out of the box with automatic color switching and seamless theme transitions.

Overview ​

Dark mode is implemented as a first-class feature:

  • Automatic: All components automatically support dark mode
  • Easy to Enable: Single CSS class toggles dark mode
  • Customizable: All colors are CSS variables
  • Accessible: Full WCAG 2.1 AA compliance in both modes
  • Performant: No JavaScript required for theme switching

Enabling Dark Mode ​

Method 1: CSS Class ​

Toggle dark mode by adding a class to the root element:

html
<!-- Light mode (default) -->
<html>
  <body><!-- Light colors --></body>
</html>

<!-- Dark mode -->
<html>
  <body class="dark"><!-- Dark colors --></body>
</html>

Method 2: Data Attribute ​

Alternative approach using data attributes:

html
<!-- Light mode (default) -->
<html data-theme="light">
  <body><!-- Light colors --></body>
</html>

<!-- Dark mode -->
<html data-theme="dark">
  <body><!-- Dark colors --></body>
</html>

Method 3: CSS Prefers-Color-Scheme ​

Automatically respect system preferences:

css
/* Light mode (default) */
:root {
  --q-primary: #2388AE;
  --dashboard-text-color: #707070;
}

/* Dark mode via system preference */
@media (prefers-color-scheme: dark) {
  :root {
    --q-primary: #34eea9;
    --dashboard-text-color: #b0b0b0;
  }
}

Vue Component Example ​

vue
<template>
  <div class="app" :class="{ dark: isDarkMode }">
    <button @click="toggleDarkMode">
      {{ isDarkMode ? '☀️' : '🌙' }}
    </button>
    <!-- App content -->
  </div>
</template>

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

const isDarkMode = ref(false)

// Detect system preference on mount
onMounted(() => {
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
  isDarkMode.value = prefersDark
})

// Listen for system preference changes
watch(isDarkMode, (newValue) => {
  // Save to localStorage
  localStorage.setItem('theme', newValue ? 'dark' : 'light')
  // Update DOM
  document.documentElement.classList.toggle('dark', newValue)
})

function toggleDarkMode() {
  isDarkMode.value = !isDarkMode.value
}
</script>

<style scoped>
.app {
  background-color: var(--dashboard-surface-color);
  color: var(--dashboard-text-color);
  transition: background-color 0.3s, color 0.3s;
}
</style>

Color Changes ​

Primary Colors ​

ModeColorHex
LightPrimary Blue#2388AE
DarkSecondary Green#34eea9

The primary color automatically inverts for dark mode to maintain visibility and brand consistency.

Table Colors ​

Light Mode:

  • Header: #ffffff background, #313131 text
  • Even rows: #fafbfc background
  • Odd rows: #ffffff background
  • Hover: rgba(35, 136, 174, 0.08) background

Dark Mode:

  • Header: #292929 background, #e4e4e4 text
  • Even rows: #2a2a2a background
  • Odd rows: #383838 background
  • Hover: rgba(35, 136, 174, 0.3) background

Text Colors ​

ElementLightDark
Primary Text#707070#b0b0b0
Disabled Text#aeaeae#5e5e5e
Borders#e9e9e9#5e5e5e
Surface#ffffff#1a1a1a

CSS Variables ​

All colors use CSS variables for easy theme customization:

css
/* Light mode (default) */
:root {
  /* Brand Colors */
  --q-primary: #2388AE;
  --q-secondary: #34eea9;
  --q-accent: #9999d6;

  /* Status Colors */
  --docbits-positive: #55b559;
  --docbits-negative: #d00000;
  --docbits-warning: #f2c037;
  --docbits-info: #31ccec;

  /* Dashboard Colors */
  --dashboard-text-color: #707070;
  --dashboard-text-disabled: #aeaeae;
  --dashboard-border-color: #aeaeae;
  --dashboard-line-color: #e9e9e9;
  --dashboard-surface-color: #ffffff;

  /* Table Colors */
  --dashboard-table-header-text: #313131;
  --dashboard-table-header-bg: #ffffff;
  --dashboard-table-row-even-bg: #fafbfc;
  --dashboard-table-row-odd-bg: #ffffff;
  --dashboard-table-row-hover-bg: rgba(35, 136, 174, 0.08);
}

/* Dark mode */
body.dark {
  --q-primary: #34eea9;
  --q-secondary: #2388AE;

  --dashboard-text-color: #b0b0b0;
  --dashboard-text-disabled: #5e5e5e;
  --dashboard-border-color: #b0b0b0;
  --dashboard-line-color: #5e5e5e;
  --dashboard-surface-color: #1a1a1a;

  --dashboard-table-header-text: #e4e4e4;
  --dashboard-table-header-bg: #292929;
  --dashboard-table-row-even-bg: #2a2a2a;
  --dashboard-table-row-odd-bg: #383838;
  --dashboard-table-row-hover-bg: rgba(35, 136, 174, 0.3);
}

Component Theme Support ​

All DocBits components automatically adapt to dark mode:

DocBitsTable ​

vue
<template>
  <!-- Automatically uses dark mode colors when dark class is present -->
  <DocBitsTable
    :rows="documents"
    :columns="columns"
    row-key="id"
  />
</template>

ColumnOrderBy ​

vue
<template>
  <!-- Sort arrows automatically change color -->
  <ColumnOrderBy
    column="name"
    :current-sort="sortColumn"
    :current-direction="sortDirection"
    @sort="handleSort"
  />
</template>

BulkActionsMenu ​

vue
<template>
  <!-- FAB and action menu adapt to dark mode -->
  <BulkActionsMenu
    :selected-count="selectedCount"
    @export="handleExport"
    @delete="handleDelete"
  />
</template>

Theme Customization ​

Override Default Colors ​

Create custom theme by overriding CSS variables:

css
/* Custom light theme */
:root {
  --q-primary: #1976d2;           /* Custom blue */
  --q-secondary: #26a69a;          /* Custom teal */
  --dashboard-text-color: #333;    /* Darker text */
  --dashboard-surface-color: #fafafa;
}

/* Custom dark theme */
body.dark {
  --q-primary: #64b5f6;            /* Light blue */
  --q-secondary: #4db6ac;          /* Light teal */
  --dashboard-text-color: #e0e0e0; /* Lighter text */
  --dashboard-surface-color: #121212;
}

Custom Theme Provider ​

Create a theme composable for Vue apps:

typescript
// composables/useTheme.ts
import { ref, computed, watch } from 'vue'

const isDarkMode = ref(false)

export function useTheme() {
  const themeClass = computed(() =>
    isDarkMode.value ? 'dark' : 'light'
  )

  function toggleTheme() {
    isDarkMode.value = !isDarkMode.value
    localStorage.setItem('theme', isDarkMode.value ? 'dark' : 'light')
  }

  function setTheme(theme: 'light' | 'dark') {
    isDarkMode.value = theme === 'dark'
    localStorage.setItem('theme', theme)
  }

  // Detect system preference on mount
  function detectSystemTheme() {
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
    isDarkMode.value = prefersDark
  }

  return {
    isDarkMode,
    themeClass,
    toggleTheme,
    setTheme,
    detectSystemTheme
  }
}

Usage in Component ​

vue
<template>
  <div :class="themeClass">
    <button @click="toggleTheme" class="theme-toggle">
      {{ isDarkMode ? '☀️ Light' : '🌙 Dark' }}
    </button>
    <DocBitsTable :rows="data" :columns="columns" />
  </div>
</template>

<script setup lang="ts">
import { useTheme } from '@/composables/useTheme'

const { isDarkMode, themeClass, toggleTheme, detectSystemTheme } = useTheme()

onMounted(() => {
  detectSystemTheme()
})
</script>

<style scoped>
div {
  background-color: var(--dashboard-surface-color);
  color: var(--dashboard-text-color);
  transition: background-color 0.3s, color 0.3s;
}

.theme-toggle {
  background-color: var(--q-primary);
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}

.theme-toggle:hover {
  opacity: 0.9;
}
</style>

Transitions ​

Smooth color transitions when switching themes:

css
/* Enable smooth transitions */
body {
  transition: background-color 0.3s ease, color 0.3s ease;
}

body * {
  transition: background-color 0.3s ease, color 0.3s ease,
              border-color 0.3s ease, box-shadow 0.3s ease;
}

Accessibility ​

Dark mode maintains full accessibility in both themes:

Color Contrast ​

CombinationLight ModeDark ModeWCAG AA
Primary text5.1:15.3:1✅ Pass
Disabled text3.2:13.5:1✅ Pass
Interactive4.8:15.2:1✅ Pass

Focus States ​

Focus indicators remain visible in both themes:

css
/* Focus state works in both modes */
button:focus-visible {
  outline: 2px solid var(--q-primary);
  outline-offset: 2px;
}

Reduced Motion ​

Respect system preference for reduced motion:

css
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

Testing Dark Mode ​

Manual Testing ​

  1. Add class="dark" to <body> element
  2. Verify all components display correctly
  3. Check color contrast meets WCAG AA
  4. Test interactive elements (buttons, links, inputs)
  5. Verify text is readable at all sizes

Unit Testing ​

typescript
import { mount } from '@vue/test-utils'
import Component from './Component.vue'

describe('Dark Mode', () => {
  it('should render correctly in dark mode', () => {
    const wrapper = mount(Component, {
      attachTo: document.body
    })

    // Add dark class
    document.body.classList.add('dark')

    // Verify colors use dark mode variables
    const element = wrapper.element as HTMLElement
    const styles = window.getComputedStyle(element)

    expect(styles.backgroundColor).toBe('rgb(26, 26, 26)') // #1a1a1a
  })

  afterEach(() => {
    document.body.classList.remove('dark')
  })
})

Visual Testing ​

Use screenshot testing to verify dark mode appearance:

typescript
import { test, expect } from '@playwright/test'

test('should match dark mode snapshot', async ({ page }) => {
  await page.goto('/components/docbits-table')

  // Enable dark mode
  await page.evaluate(() => {
    document.body.classList.add('dark')
  })

  // Take screenshot
  await expect(page).toHaveScreenshot('docbits-table-dark.png')
})

Browser Support ​

Dark mode is supported in all modern browsers:

BrowserSupport
Chrome 76+✅ Full
Firefox 67+✅ Full
Safari 12.1+✅ Full
Edge 79+✅ Full

Performance ​

Dark mode has minimal performance impact:

  • CSS variable switching: <1ms
  • No JavaScript required for theme change
  • Transitions are GPU-accelerated
  • System preference detection is efficient

Best Practices ​

1. Use CSS Variables ​

Always use CSS variables for colors:

css
/* ✅ Good: Uses CSS variables */
.button {
  background-color: var(--q-primary);
  color: white;
}

/* ❌ Bad: Hardcoded colors */
.button {
  background-color: #2388AE;
  color: white;
}

2. Provide Escape Hatch ​

Let users explicitly choose theme:

vue
<script setup lang="ts">
const userThemePreference = ref<'light' | 'dark' | 'auto'>('auto')

watch(userThemePreference, (preference) => {
  if (preference === 'auto') {
    // Use system preference
  } else {
    // Use explicit preference
  }
})
</script>

3. Test Both Modes ​

Always test your components in both light and dark modes:

bash
# Visual regression testing for both themes
npm run test:visual -- --theme light
npm run test:visual -- --theme dark

4. Respect User Preference ​

Honor user's system preference as default:

typescript
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const initialTheme = prefersDark ? 'dark' : 'light'

DocBits Component Library