Open demo

CSS code

HTML
<div class="lf">
<main class="lf-box">
  <button class="lf-tog" id="lf-theme" type="button" aria-label="Dark theme" aria-pressed="false">
    <svg class="lf-moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 019.5 4a8 8 0 1010.5 10.5z"/></svg>
    <svg class="lf-sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4.2"/><path d="M12 2v2.4M12 19.6V22M4.2 4.2l1.7 1.7M18.1 18.1l1.7 1.7M2 12h2.4M19.6 12H22M4.2 19.8l1.7-1.7M18.1 5.9l1.7-1.7"/></svg>
  </button>
  <div class="lf-mark" aria-hidden="true">N</div>
  <h1>Sign in</h1>
  <p class="lf-sub">This screen follows your system theme until you change it.</p>
  <form onsubmit="event.preventDefault()">
    <label class="lf-label" for="dmail">Email address</label>
    <input class="lf-field" id="dmail" name="username" type="email" autocomplete="username" placeholder="[email protected]" required>
    <label class="lf-label" for="dpw">Password</label>
    <input class="lf-field" id="dpw" name="password" type="password" autocomplete="current-password" placeholder="••••••••" required>
    <div class="lf-row">
      <label class="lf-check"><input type="checkbox" name="remember"> Remember me</label>
      <a href="#">Forgot password?</a>
    </div>
    <button class="lf-btn" type="submit">Sign in</button>
  </form>
  <p class="lf-foot">No account? <a href="#">Create one</a></p>
</main>
</div>
CSS
/* Theme-aware login. The colours are custom properties on :root: prefers-color-scheme picks light or dark,
   and data-theme="light|dark" on <html> (set by the toggle) overrides it. Without JavaScript it still follows the OS. */
:root{--lf-bg:#f4f5f9;--lf-card:#fff;--lf-edge:#e4e6ef;--lf-field-edge:#868c99;--lf-txt:#111827;--lf-dim:#6b7280;--lf-inp:#fff;
  --lf-acc:#4f46e5;--lf-acc-text:#fff;--lf-link:#4f46e5;--lf-ring:rgba(79,70,229,.18);--lf-sun:none;--lf-moon:block;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){
  --lf-bg:#0e1117;--lf-card:#161a23;--lf-edge:#262c39;--lf-field-edge:#6b7488;--lf-txt:#e8eaf1;--lf-dim:#98a0b3;--lf-inp:#1c212c;
  --lf-acc:#5b5fe6;--lf-link:#a5b4fc;--lf-ring:rgba(165,180,252,.25);--lf-sun:block;--lf-moon:none;color-scheme:dark}}
:root[data-theme=dark]{--lf-bg:#0e1117;--lf-card:#161a23;--lf-edge:#262c39;--lf-field-edge:#6b7488;--lf-txt:#e8eaf1;--lf-dim:#98a0b3;--lf-inp:#1c212c;
  --lf-acc:#5b5fe6;--lf-link:#a5b4fc;--lf-ring:rgba(165,180,252,.25);--lf-sun:block;--lf-moon:none;color-scheme:dark}
.lf{min-height:100vh;display:grid;grid-template-columns:minmax(0,1fr);place-items:center;padding:28px 20px;
  background:var(--lf-bg);color:var(--lf-txt);line-height:normal;transition:background .2s,color .2s;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
.lf,.lf *,.lf *::before,.lf *::after{box-sizing:border-box}
.lf :where(h1,p,form){margin:0}
.lf :where(input,button){font:inherit}
.lf a{color:var(--lf-link);text-decoration:none;font-weight:600}
.lf a:hover{text-decoration:underline}
.lf :focus-visible{outline:2px solid var(--lf-link);outline-offset:2px}
.lf-box{width:100%;max-width:400px;background:var(--lf-card);border:1px solid var(--lf-edge);border-radius:16px;
  padding:34px 32px;position:relative;transition:background .2s,border-color .2s}
.lf-tog{position:absolute;top:16px;right:16px;width:36px;height:36px;border-radius:10px;cursor:pointer;
  background:none;border:1px solid var(--lf-edge);color:var(--lf-dim);display:grid;place-items:center;transition:background .15s,color .15s}
.lf-tog:hover{background:var(--lf-bg);color:var(--lf-txt)}
.lf-tog svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:2}
.lf-tog .lf-sun{display:var(--lf-sun)}
.lf-tog .lf-moon{display:var(--lf-moon)}
.lf-mark{width:40px;height:40px;border-radius:11px;background:var(--lf-acc);color:var(--lf-acc-text);
  display:grid;place-items:center;font-weight:800;margin-bottom:20px}
.lf h1{font-size:1.36rem;letter-spacing:-.02em;margin-bottom:6px}
.lf-sub{color:var(--lf-dim);font-size:.91rem;margin-bottom:24px}
.lf-label{display:block;font-size:.84rem;font-weight:600;margin-bottom:6px}
.lf-field{width:100%;padding:12px 14px;border:1px solid var(--lf-field-edge);border-radius:10px;font-size:.95rem;
  margin-bottom:17px;background:var(--lf-inp);color:var(--lf-txt);transition:background .2s,border-color .15s,box-shadow .15s}
.lf-field::placeholder{color:var(--lf-dim)}
.lf-field:focus{outline:none;border-color:var(--lf-acc);box-shadow:0 0 0 3px var(--lf-ring)}
.lf-row{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:22px;font-size:.86rem;color:var(--lf-dim)}
.lf-check{display:flex;align-items:center;gap:7px;font-weight:500;cursor:pointer}
.lf-check input{margin:0;accent-color:var(--lf-acc)}
.lf-btn{width:100%;padding:13px;border:0;border-radius:10px;background:var(--lf-acc);color:var(--lf-acc-text);
  font-size:.96rem;font-weight:700;cursor:pointer}
.lf-btn:hover{filter:brightness(1.08)}
.lf-foot{margin-top:22px;text-align:center;font-size:.88rem;color:var(--lf-dim)}
@media (prefers-reduced-motion:reduce){.lf,.lf-box,.lf-tog,.lf-field{transition:none}}
JavaScript
// No data-theme attribute means the media query decides; the toggle only stamps <html> once someone
// opts out. aria-pressed tells assistive technology whether the dark theme is on.
(() => {
  const root = document.documentElement, tog = document.getElementById('lf-theme');
  if (!tog) return;
  const mq = matchMedia('(prefers-color-scheme: dark)');
  const dark = () => root.dataset.theme ? root.dataset.theme === 'dark' : mq.matches;
  const sync = () => tog.setAttribute('aria-pressed', String(dark()));
  tog.addEventListener('click', () => { root.dataset.theme = dark() ? 'light' : 'dark'; sync(); });
  mq.addEventListener('change', sync);
  sync();
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="lf d-grid">
<main class="card w-100">
  <div class="card-body">
    <button class="btn btn-theme position-absolute" id="lf-theme" type="button" aria-label="Dark theme" aria-pressed="false">
      <svg class="lf-moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 019.5 4a8 8 0 1010.5 10.5z"/></svg>
      <svg class="lf-sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4.2"/><path d="M12 2v2.4M12 19.6V22M4.2 4.2l1.7 1.7M18.1 18.1l1.7 1.7M2 12h2.4M19.6 12H22M4.2 19.8l1.7-1.7M18.1 5.9l1.7-1.7"/></svg>
    </button>
    <div class="lf-mark" aria-hidden="true">N</div>
    <h1>Sign in</h1>
    <p class="lf-sub text-body-secondary">This screen follows your system theme until you change it.</p>
    <form onsubmit="event.preventDefault()">
      <label class="form-label" for="dmail">Email address</label>
      <input class="form-control" id="dmail" name="username" type="email" autocomplete="username" placeholder="[email protected]" required>
      <label class="form-label" for="dpw">Password</label>
      <input class="form-control" id="dpw" name="password" type="password" autocomplete="current-password" placeholder="••••••••" required>
      <div class="lf-row d-flex justify-content-between align-items-center">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" id="remember" name="remember">
          <label class="form-check-label" for="remember">Remember me</label>
        </div>
        <a href="#">Forgot password?</a>
      </div>
      <button class="btn btn-brand w-100" type="submit">Sign in</button>
    </form>
    <p class="lf-foot text-body-secondary text-center mb-0">No account? <a href="#">Create one</a></p>
  </div>
</main>
</div>
CSS
/* Bootstrap .card, .form-control, .form-check and .btn. The --lf-* tokens switch with [data-bs-theme]
   and feed Bootstrap's own variables, so its components follow the theme too. */
.lf{--lf-bg:#f4f5f9;--lf-card:#fff;--lf-edge:#e4e6ef;--lf-field-edge:#868c99;--lf-txt:#111827;--lf-dim:#6b7280;--lf-inp:#fff;
  --lf-acc:#4f46e5;--lf-acc-rgb:79,70,229;--lf-link-rgb:79,70,229;--lf-link:#4f46e5;--lf-ring:rgba(79,70,229,.18);--lf-sun:none;--lf-moon:block}
[data-bs-theme=dark] .lf{--lf-bg:#0e1117;--lf-card:#161a23;--lf-edge:#262c39;--lf-field-edge:#6b7488;--lf-txt:#e8eaf1;--lf-dim:#98a0b3;--lf-inp:#1c212c;
  --lf-acc:#5b5fe6;--lf-link-rgb:165,180,252;--lf-link:#a5b4fc;--lf-ring:rgba(165,180,252,.25);--lf-sun:block;--lf-moon:none}
.lf{--bs-body-color:var(--lf-txt);--bs-body-bg:var(--lf-inp);--bs-secondary-color:var(--lf-dim);--bs-border-color:var(--lf-field-edge);
  --bs-link-color-rgb:var(--lf-link-rgb);--bs-link-hover-color-rgb:var(--lf-link-rgb);
  min-height:100vh;grid-template-columns:minmax(0,1fr);place-items:center;padding:28px 20px;
  background:var(--lf-bg);color:var(--lf-txt);line-height:normal;transition:background-color .2s,color .2s}
.lf a{font-weight:600;text-decoration:none}
.lf a:hover{text-decoration:underline}
.lf :focus-visible{outline:2px solid var(--lf-link);outline-offset:2px}
.lf .card{--bs-card-bg:var(--lf-card);--bs-card-border-color:var(--lf-edge);--bs-card-border-radius:16px;--bs-card-spacer-y:34px;--bs-card-spacer-x:32px;
  max-width:400px;transition:background-color .2s,border-color .2s}
.lf .btn-theme{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-radius:10px;--bs-btn-border-color:var(--lf-edge);--bs-btn-color:var(--lf-dim);
  --bs-btn-hover-bg:var(--lf-bg);--bs-btn-hover-border-color:var(--lf-edge);--bs-btn-hover-color:var(--lf-txt);
  --bs-btn-active-bg:var(--lf-bg);--bs-btn-active-border-color:var(--lf-edge);--bs-btn-active-color:var(--lf-txt);--bs-btn-focus-box-shadow:none;
  top:16px;right:16px;width:36px;height:36px;display:grid;place-items:center;transition:background-color .15s,color .15s}
.lf .btn-theme svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:2}
.lf .btn-theme .lf-sun{display:var(--lf-sun)}
.lf .btn-theme .lf-moon{display:var(--lf-moon)}
.lf-mark{width:40px;height:40px;border-radius:11px;background:var(--lf-acc);color:#fff;display:grid;place-items:center;font-weight:800;margin-bottom:20px}
.lf h1{font-size:1.36rem;font-weight:700;line-height:normal;letter-spacing:-.02em;margin-bottom:6px}
.lf-sub{font-size:.91rem;margin-bottom:24px}
.lf .form-label{font-size:.84rem;font-weight:600;margin-bottom:6px}
.lf .form-control{padding:12px 14px;border-radius:10px;font-size:.95rem;line-height:normal;margin-bottom:17px;transition:background-color .2s,border-color .15s,box-shadow .15s}
.lf .form-control:focus{background-color:var(--lf-inp);color:var(--lf-txt);border-color:var(--lf-acc);box-shadow:0 0 0 3px var(--lf-ring);outline:none}
.lf .form-control::placeholder{color:var(--lf-dim)}
.lf-row{gap:12px;margin-bottom:22px;font-size:.86rem}
.lf .form-check{display:flex;align-items:center;gap:7px;padding:0;margin:0;min-height:0}
.lf .form-check-input{float:none;margin:0;cursor:pointer}
.lf .form-check-input:checked{background-color:var(--lf-acc);border-color:var(--lf-acc)}
.lf .form-check-input:focus{border-color:var(--lf-acc);box-shadow:none}
.lf .form-check-label{font-weight:500;color:var(--lf-dim);cursor:pointer}
.lf .btn-brand{--bs-btn-line-height:normal;--bs-btn-border-width:0;--bs-btn-padding-y:13px;--bs-btn-font-size:.96rem;--bs-btn-font-weight:700;--bs-btn-border-radius:10px;
  --bs-btn-bg:var(--lf-acc);--bs-btn-border-color:var(--lf-acc);--bs-btn-color:#fff;
  --bs-btn-hover-bg:var(--lf-acc);--bs-btn-hover-border-color:var(--lf-acc);--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:var(--lf-acc);--bs-btn-active-border-color:var(--lf-acc);--bs-btn-active-color:#fff;--bs-btn-focus-box-shadow:none}
.lf .btn-brand:hover{filter:brightness(1.08)}
.lf-foot{margin-top:22px;font-size:.88rem}
@media (prefers-reduced-motion:reduce){.lf,.lf .card,.lf .btn,.lf .form-control{transition:none}}
JavaScript
// Bootstrap colour modes: data-bs-theme on <html>. With no saved choice it follows the OS, and it runs in <head>
// so the page never flashes the wrong theme. The toggle overrides it for the rest of the visit.
(() => {
  const root = document.documentElement, mq = matchMedia('(prefers-color-scheme: dark)');
  let choice = null;
  const theme = () => choice || (mq.matches ? 'dark' : 'light');
  const apply = () => {
    root.setAttribute('data-bs-theme', theme());
    const tog = document.getElementById('lf-theme');
    if (tog) tog.setAttribute('aria-pressed', String(theme() === 'dark'));
  };
  apply();
  mq.addEventListener('change', apply);
  document.addEventListener('DOMContentLoaded', () => {
    const tog = document.getElementById('lf-theme');
    if (!tog) return;
    tog.addEventListener('click', () => { choice = theme() === 'dark' ? 'light' : 'dark'; apply(); });
    apply();
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid min-h-screen grid-cols-[minmax(0,1fr)] place-items-center bg-page px-5 py-7 font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] leading-[normal] text-ink scheme-light transition-[background-color,border-color,color] duration-200 motion-reduce:transition-none dark:bg-page-night dark:text-ink-night dark:scheme-dark">
<main class="relative w-full max-w-[400px] rounded-2xl border border-edge bg-white px-8 py-[34px] transition-[background-color,border-color,color] duration-200 motion-reduce:transition-none dark:border-edge-night dark:bg-card-night">
  <button class="group absolute top-4 right-4 grid size-9 cursor-pointer place-items-center rounded-[10px] border border-edge text-dim transition-[background-color,border-color,color] duration-150 hover:bg-page hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand motion-reduce:transition-none dark:border-edge-night dark:text-dim-night dark:hover:bg-page-night dark:hover:text-ink-night dark:focus-visible:outline-link-night" id="lf-theme" type="button" aria-label="Dark theme" aria-pressed="false">
    <svg class="size-[17px] fill-none stroke-current stroke-2 dark:hidden" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.5A8 8 0 019.5 4a8 8 0 1010.5 10.5z"/></svg>
    <svg class="hidden size-[17px] fill-none stroke-current stroke-2 dark:block" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4.2"/><path d="M12 2v2.4M12 19.6V22M4.2 4.2l1.7 1.7M18.1 18.1l1.7 1.7M2 12h2.4M19.6 12H22M4.2 19.8l1.7-1.7M18.1 5.9l1.7-1.7"/></svg>
  </button>
  <div class="mb-5 grid size-10 place-items-center rounded-[11px] bg-brand font-extrabold text-white dark:bg-brand-night" aria-hidden="true">N</div>
  <h1 class="mb-1.5 text-[1.36rem] font-bold tracking-[-.02em]">Sign in</h1>
  <p class="mb-6 text-[.91rem] text-dim dark:text-dim-night">This screen follows your system theme until you change it.</p>
  <form onsubmit="event.preventDefault()">
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="dmail">Email address</label>
    <input class="mb-[17px] w-full rounded-[10px] border border-field bg-white px-3.5 py-3 text-[.95rem] transition-[background-color,border-color,box-shadow] duration-150 placeholder:text-dim focus:border-brand focus:shadow-[0_0_0_3px_rgba(79,70,229,.18)] focus:outline-none motion-reduce:transition-none dark:border-field-night dark:bg-inp-night dark:placeholder:text-dim-night dark:focus:border-brand-night dark:focus:shadow-[0_0_0_3px_rgba(165,180,252,.25)]"
           id="dmail" name="username" type="email" autocomplete="username" placeholder="[email protected]" required>
    <label class="mb-1.5 block text-[.84rem] font-semibold" for="dpw">Password</label>
    <input class="mb-[17px] w-full rounded-[10px] border border-field bg-white px-3.5 py-3 text-[.95rem] transition-[background-color,border-color,box-shadow] duration-150 placeholder:text-dim focus:border-brand focus:shadow-[0_0_0_3px_rgba(79,70,229,.18)] focus:outline-none motion-reduce:transition-none dark:border-field-night dark:bg-inp-night dark:placeholder:text-dim-night dark:focus:border-brand-night dark:focus:shadow-[0_0_0_3px_rgba(165,180,252,.25)]"
           id="dpw" name="password" type="password" autocomplete="current-password" placeholder="••••••••" required>
    <div class="mb-[22px] flex items-center justify-between gap-3 text-[.86rem] text-dim dark:text-dim-night">
      <label class="flex cursor-pointer items-center gap-[7px] font-medium"><input class="accent-brand focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand dark:accent-brand-night dark:focus-visible:outline-link-night" type="checkbox" name="remember"> Remember me</label>
      <a class="font-semibold text-brand hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand dark:text-link-night dark:focus-visible:outline-link-night" href="#">Forgot password?</a>
    </div>
    <button class="w-full cursor-pointer rounded-[10px] bg-brand p-[13px] text-[.96rem] font-bold text-white hover:brightness-108 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand dark:bg-brand-night dark:focus-visible:outline-link-night" type="submit">Sign in</button>
  </form>
  <p class="mt-[22px] text-center text-[.88rem] text-dim dark:text-dim-night">No account? <a class="font-semibold text-brand hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand dark:text-link-night dark:focus-visible:outline-link-night" href="#">Create one</a></p>
</main>
</div>
CSS
/* dark: follows prefers-color-scheme, and data-theme="light|dark" on <html> (set by the toggle) overrides it. */
@custom-variant dark {
  &:where([data-theme=dark], [data-theme=dark] *) { @slot; }
  @media (prefers-color-scheme: dark) {
    &:where(:root:not([data-theme=light]), :root:not([data-theme=light]) *) { @slot; }
  }
}
@theme {
  --color-brand: #4f46e5;
  --color-brand-night: #5b5fe6;
  --color-link-night: #a5b4fc;
  --color-ink: #111827;
  --color-ink-night: #e8eaf1;
  --color-dim: #6b7280;
  --color-dim-night: #98a0b3;
  --color-edge: #e4e6ef;
  --color-edge-night: #262c39;
  --color-field: #868c99;
  --color-field-night: #6b7488;
  --color-page: #f4f5f9;
  --color-page-night: #0e1117;
  --color-card-night: #161a23;
  --color-inp-night: #1c212c;
}
JavaScript
// No data-theme attribute means the media query decides; the toggle only stamps <html> once someone
// opts out. aria-pressed tells assistive technology whether the dark theme is on.
(() => {
  const root = document.documentElement, tog = document.getElementById('lf-theme');
  if (!tog) return;
  const mq = matchMedia('(prefers-color-scheme: dark)');
  const dark = () => root.dataset.theme ? root.dataset.theme === 'dark' : mq.matches;
  const sync = () => tog.setAttribute('aria-pressed', String(dark()));
  tog.addEventListener('click', () => { root.dataset.theme = dark() ? 'light' : 'dark'; sync(); });
  mq.addEventListener('change', sync);
  sync();
})();

About this set

A login card with light and dark themes that follows the operating system until the person picks one. It holds a brand mark, email and password fields, a remember-me checkbox, a forgotten-password link, a Sign in button and a sign-up link, with a small square toggle in the top-right corner that shows a moon in light mode and a sun in dark mode. In plain CSS every colour is a custom property on :root; prefers-color-scheme swaps the set, and a data-theme attribute that the toggle writes on the html element overrides the media query, so the page follows the OS with JavaScript turned off. color-scheme is set too, so native checkboxes and autofill match. The Tailwind version redefines the dark variant to honour the same attribute and media query. Bootstrap’s colour modes are attribute-driven, so its version runs a few lines in the head that set data-bs-theme from the OS before first paint, then let the toggle override it. The choice lasts for the visit; add storage if you want it remembered.

What’s included

  • Light and dark themes from prefers-color-scheme
  • Toggle with aria-pressed that overrides the OS setting
  • Dark indigo and link colours tuned for 4.5:1 text contrast
  • color-scheme set so native controls match the theme
  • Bootstrap data-bs-theme and a custom Tailwind dark variant

Accessibility

The toggle is a button named Dark theme with aria-pressed reflecting the current state, and its icons are hidden from assistive technology. Fields have visible labels and username and current-password autocomplete. Focus outlines switch to a light indigo in dark mode so they stay visible, and colour transitions are removed under prefers-reduced-motion.

Customise it

Edit the --lf-* tokens in the :root and dark blocks (plain CSS), the --lf-* values under .lf and [data-bs-theme=dark] .lf (Bootstrap), or the paired --color-* and --color-*-night theme values (Tailwind).