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