Theme

The library supports light and dark modes out of the box. Colour tokens resolve automatically based on the OS preference, or you can force a specific theme with the data-theme attribute.

How it works

All colour tokens are defined with the CSS light-dark() function. The root element declares color-scheme: light dark, which lets the browser resolve every token to its light or dark value based on the operating system preference — no JavaScript required.

styles.csscss
:root {
  color-scheme: light dark;
  --canvas: light-dark(#FAFAFA, #09090B);
  --text:   light-dark(#09090B, #FAFAFA);
  /* … */
}

Forcing a theme

Set data-theme on any ancestor element to override the OS preference. Use data-theme="dark" or data-theme="light".

index.htmlhtml
<!-- Force dark on the whole page -->
<html data-theme="dark">…</html>

<!-- Light island inside a dark page -->
<section data-theme="light">…</section>

When data-theme="light" is set, the element gets color-scheme: light, forcing all light-dark() tokens to their first value. The opposite applies for data-theme="dark".

Theme toggle with persistence

A common pattern is to let users pick their preferred theme and store it in localStorage. When the stored preference differs from the OS setting, the page can flash the wrong theme on load because the CSS resolves tokens before JavaScript runs.

The library ships a small blocking script, theme-init.js, that solves this. It reads the stored preference and sets data-theme on <html> synchronously — before the browser paints.

jsjs
// theme-init.js (shipped in ln/dist/)
(function() {
  try {
    var t = localStorage.getItem("ln:theme");
    if (t === "light" || t === "dark") {
      document.documentElement.dataset.theme = t;
    }
  } catch(e) {}
})();

Usage

Add the script to your document <head> before any stylesheets. It must be a blocking (non-deferred) script so it runs before the first paint.

index.htmlhtml
<head>
  <script src="/theme-init.js"></script>
  <link rel="stylesheet" href="/styles.css" />
</head>

When your toggle writes a new preference, store it under the same key the script reads:

jsjs
// Toggle the theme
const next = current === "dark" ? "light" : "dark";
document.documentElement.dataset.theme = next;
localStorage.setItem("ln:theme", next);

If no preference is stored, the script does nothing and the browser’s default color-scheme: light dark resolves tokens based on the OS setting — no flash, no fallback needed.

React / Next.js

In a React framework with server rendering, the blocking script sets data-theme before hydration. Add suppressHydrationWarning to <html> so React does not warn about the mismatch between the server-rendered markup and the client DOM.

component.tsxtsx
export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script src="/theme-init.js" />
        <link rel="stylesheet" href="/styles.css" />
      </head>
      <body>{children}</body>
    </html>
  );
}

Your toggle component should read the already-applied data-theme attribute inside useLayoutEffect (not useEffect) so the icon updates before the browser paints, avoiding a visual flicker.