Tailwind dark variant for non-daisyui components

Hi everybody,

I was evaluating phoenix version 1.8.0-rc.0 together with the newly updated version of fluxon and trying to get the dark mode selection to automatically apply to the fluxon components.

I got it working with the following changes:

Add the following line to app.css:

@custom-variant dark (&:where(.dark, .dark *));

Add the following to the setTheme function in root.html.heex:

if ((theme === "system" && window.matchMedia('(prefers-color-scheme: dark)').matches) || theme === "dark") {
  document.documentElement.classList.add('dark');
} else {
  document.documentElement.classList.remove('dark');
}

Since more people might be using tailwinds dark variant on their components, this might be something to include in phoenix by default, to increase adoption. In previous versions of phoenix, a light/dark mode selector was a something you had to build yourself. But now that we have the daisyui theme switcher, it might as well be enabled for other tailwind components by default.

Curious to hear your thoughts.

1 Like

I’m working on my personal website and it’s the first time I really use full blown TailwindCSS (used it several times for prototyping but always ended up using my “own framework” on production projects). I simply followed the suggestion of Tailwind but opted to go the attribute route.

My root script is the following:

<script type="text/javascript">
          (() => {
        const storageKey = 'phx:theme';
        const getSystemTheme = () => {
          return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
        }
        const setTheme = (theme) => {
          if (theme === 'system') {
            localStorage.removeItem(storageKey);
            document.documentElement.setAttribute('data-theme', getSystemTheme());
            document.documentElement.setAttribute('data-theme-mode', 'system');
          } else {
            localStorage.setItem(storageKey, theme);
            document.documentElement.setAttribute('data-theme', theme);
            document.documentElement.setAttribute('data-theme-mode', 'user');
          }
        };

        setTheme(localStorage.getItem(storageKey) || 'system');

        window.addEventListener('storage', (e) => e.key === storageKey && setTheme(e.newValue || 'system'));
        window.addEventListener('phx:set-theme', ({ detail: { theme } }) => setTheme(theme));
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
          if (!localStorage.getItem(storageKey)) { setTheme('system'); }
        });
      })();
    </script>

Then I have a theme.css file that I import into the app.css main file with the following:

/* Theming */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

/* Typography */
@theme {
  --font-sans: 'Inter', 'Segoe UI', 'Noto Sans', Helvetica, Arial, ui-sans-serif, system-ui,
    sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  --font-headings: 'Montserrat', Georgia, Cambria, 'Times New Roman', Times, ui-serif, serif;
}

/* Colors */
@theme {
  --neutral-base: var(--color-zinc-500);

  /* Surface */
  --color-surface-10: oklch(0.985 0 0);
  --color-surface-20: oklch(0.94 0 0);
  --color-surface-30: oklch(0.88 0 0);
  --color-surface-40: oklch(0.82 0 0);

  /* Content */
  --color-content-10: oklch(from var(--neutral-base) 0.28 0 h);
  --color-content-20: oklch(from var(--neutral-base) 0.44 0 h);
  --color-content-30: oklch(from var(--neutral-base) 0.62 0 h);
  --color-content-40: oklch(from var(--neutral-base) 0.72 0 h);

  /* Gray (Neutral) */
  --color-gray-*: initial;
  --color-gray: var(--neutral-base);
  --color-gray-50: oklch(from var(--color-gray) 0.985 calc(c * 0.05) calc(h * 0.96));
  --color-gray-100: oklch(from var(--color-gray) 0.97 calc(c * 0.15) calc(h * 0.965));
  --color-gray-200: oklch(from var(--color-gray) 0.93 calc(c * 0.3) calc(h * 0.97));
  --color-gray-300: oklch(from var(--color-gray) 0.87 calc(c * 0.5) calc(h * 0.98));
  --color-gray-400: oklch(from var(--color-gray) 0.7 calc(c * 0.9) calc(h * 0.998));
  --color-gray-500: var(--color-gray);
  --color-gray-600: oklch(from var(--color-gray) 0.45 calc(c * 0.95) calc(h * 1.01));
  --color-gray-700: oklch(from var(--color-gray) 0.37 calc(c * 0.94) calc(h * 1.02));
  --color-gray-800: oklch(from var(--color-gray) 0.28 calc(c * 0.935) calc(h * 1.03));
  --color-gray-900: oklch(from var(--color-gray) 0.2 calc(c * 0.93) calc(h * 1.04));
  --color-gray-950: oklch(from var(--color-gray) 0.13 calc(c * 0.92) calc(h * 1.05));

  /* Primary */
  --color-primary: oklch(0.62 0.17 23);
  --color-primary-light: oklch(from var(--color-primary) calc(l * 1.15) calc(c * 0.9) h);
  --color-primary-lighter: oklch(from var(--color-primary) calc(l * 1.35) calc(c * 0.5) h);
  --color-primary-dark: oklch(from var(--color-primary) calc(l * 0.86) calc(c * 0.9) h);
  --color-primary-darker: oklch(from var(--color-primary) calc(l * 0.7) calc(c * 0.75) h);
  --color-primary-contrast: oklch(from var(--color-primary) 99% 1% h);

  /* Secondary */
  --color-secondary: oklch(from var(--color-primary) l c calc(h + 180));
  --color-secondary-light: oklch(from var(--color-secondary) calc(l * 1.15) calc(c * 0.9) h);
  --color-secondary-lighter: oklch(from var(--color-secondary) calc(l * 1.35) calc(c * 0.5) h);
  --color-secondary-dark: oklch(from var(--color-secondary) calc(l * 0.85) calc(c * 0.9) h);
  --color-secondary-darker: oklch(from var(--color-secondary) calc(l * 0.7) calc(c * 0.75) h);
  --color-secondary-contrast: oklch(from var(--color-secondary) 99% 1% h);

  /* Info Intent */
  --color-info: oklch(0.74 0.16 232.661);
  --color-info-light: oklch(from var(--color-info) calc(l * 1.15) calc(c * 0.9) h);
  --color-info-lighter: oklch(from var(--color-info) calc(l * 1.35) calc(c * 0.5) h);
  --color-info-dark: oklch(from var(--color-info) calc(l * 0.85) calc(c * 0.9) h);
  --color-info-darker: oklch(from var(--color-info) calc(l * 0.7) calc(c * 0.75) h);
  --color-info-contrast: oklch(from var(--color-info) 99% 1% h);

  /* Success Intent */
  --color-success: oklch(0.76 0.177 163.223);
  --color-success-light: oklch(from var(--color-success) calc(l * 1.15) calc(c * 0.9) h);
  --color-success-lighter: oklch(from var(--color-success) calc(l * 1.35) calc(c * 0.5) h);
  --color-success-dark: oklch(from var(--color-success) calc(l * 0.85) calc(c * 0.9) h);
  --color-success-darker: oklch(from var(--color-success) calc(l * 0.7) calc(c * 0.75) h);
  --color-success-contrast: oklch(from var(--color-success) 99% 1% h);

  /* Warning Intent */
  --color-warning: oklch(0.82 0.189 84.429);
  --color-warning-light: oklch(from var(--color-warning) calc(l * 1.15) calc(c * 0.9) h);
  --color-warning-lighter: oklch(from var(--color-warning) calc(l * 1.35) calc(c * 0.5) h);
  --color-warning-dark: oklch(from var(--color-warning) calc(l * 0.85) calc(c * 0.9) h);
  --color-warning-darker: oklch(from var(--color-warning) calc(l * 0.7) calc(c * 0.75) h);
  --color-warning-contrast: oklch(from var(--color-warning) 99% 1% h);

  /* Danger Intent */
  --color-danger: oklch(0.7 0.191 22.216);
  --color-danger-light: oklch(from var(--color-danger) calc(l * 1.15) calc(c * 0.9) h);
  --color-danger-lighter: oklch(from var(--color-danger) calc(l * 1.35) calc(c * 0.5) h);
  --color-danger-dark: oklch(from var(--color-danger) calc(l * 0.85) calc(c * 0.9) h);
  --color-danger-darker: oklch(from var(--color-danger) calc(l * 0.7) calc(c * 0.75) h);
  --color-danger-contrast: oklch(from var(--color-danger) 99% 1% h);
}

/* Elements */
@theme inline {
  --border: 1px;
  --shadow-field: var(--shadow-sm);
  --radius-field: 0.5rem;
  --radius-selector: 0.25rem;
  /* --radius-box: 0.5rem; */
  /* --size-field: 0.21875rem; */
  /* --size-selector: 0.21875rem; */

  /* Button */
  --btn-shadow: var(--shadow-field);
  --btn-radius: var(--radius-field);
  --btn-border: var(--border);

  /* Badge */
  --badge-radius: 9999px;
}

/* Dark Theme */
@layer base {
  [data-theme='dark'] {
    --color-surface-10: oklch(from var(--neutral-base) 0.18 0.005 h);
    --color-surface-20: oklch(from var(--neutral-base) 0.22 0.006 h);
    --color-surface-30: oklch(from var(--neutral-base) 0.28 0.006 h);
    --color-surface-40: oklch(from var(--neutral-base) 0.36 0.004 h);

    --color-content-10: oklch(from var(--neutral-base) 0.86 0.001 h);
    --color-content-20: oklch(from var(--neutral-base) 0.74 0 h);
    --color-content-30: oklch(from var(--neutral-base) 0.64 0 h);
    --color-content-40: oklch(from var(--neutral-base) 0.52 0 h);
  }
}

I had done something similar first, but then I found that the phoenix theme selector would not move to system anymore, because it is based on the data-theme attribute. That’s why I switched to using the dark class.

You can easily change that. This is what I’ve done. Thought I’m not using it as I change themes in a different way.

  @doc """
  Provides dark vs light theme toggle based on themes defined in app.css.
  See <head> in root.html.heex which applies the theme before page load.
  """
  def theme_toggle(assigns) do
    ~H"""
    <div class="relative flex flex-row items-center border-1 border-surface-30 bg-surface-20 rounded-full">
      <div class="absolute w-[33.33%] h-full rounded-full border-1 border-surface-30 bg-surface-10 brightness-110 left-0
      [[data-theme-mode=user][data-theme=light]_&]:left-[33.33%] [[data-theme-mode=user][data-theme=dark]_&]:left-[66.66%] transition-[left]" />

      <button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "system"})} class="flex p-2">
        <.icon name="hero-computer-desktop-micro" class="size-4 opacity-75 hover:opacity-100" />
      </button>

      <button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "light"})} class="flex p-2">
        <.icon name="hero-sun-micro" class="size-4 opacity-75 hover:opacity-100" />
      </button>

      <button phx-click={JS.dispatch("phx:set-theme", detail: %{theme: "dark"})} class="flex p-2">
        <.icon name="hero-moon-micro" class="size-4 opacity-75 hover:opacity-100" />
      </button>
    </div>
    """
  end

I added another attribute called that-theme-mode, that is either ‘system’ or ‘user’, where system is well, system based and ‘user’ is user selected, be it ‘light’, ‘dark’ or any other theme keyword.