diff --git a/gatsby/package.json b/gatsby/package.json
index 12d1562e754b3fa13be4f425523dce40f9cfff55..42f58315bb58d0d0fa0f0c7620549db2392cba0f 100644
--- a/gatsby/package.json
+++ b/gatsby/package.json
@@ -26,6 +26,7 @@
     "@sikt/sds-logo": "^2.0.0",
     "@sikt/sds-section": "^3.0.0",
     "@sikt/sds-table": "^2.0.1",
+    "@sikt/sds-toggle": "^2.1.0",
     "@sikt/sds-tokens": "^1.0.0",
     "canvas-confetti": "^1.9.2",
     "clsx": "^2.1.0",
diff --git a/gatsby/src/components/Footer.tsx b/gatsby/src/components/Footer.tsx
index 8bd21fc176ab22e2544d74a29d46f980461be90c..3aebf79a200d1d9644d3784ad18f8358662e5a02 100644
--- a/gatsby/src/components/Footer.tsx
+++ b/gatsby/src/components/Footer.tsx
@@ -3,6 +3,7 @@ import { Footer as SdsFooter } from "@sikt/sds-footer";
 import * as style from "./footer.module.css";
 import { ButtonLink } from "@sikt/sds-button";
 import { Link } from "@sikt/sds-core";
+import { ToggleSwitchColorScheme } from "@sikt/sds-toggle";
 import clsx from "clsx";
 
 const Footer = ({ className }: { className?: string }) => {
@@ -47,6 +48,7 @@ const Footer = ({ className }: { className?: string }) => {
           </li>
         </ul>
       </div>
+      <ToggleSwitchColorScheme control="internal" label="Velg tema" />
     </SdsFooter>
   );
 };
diff --git a/gatsby/src/components/footer.module.css b/gatsby/src/components/footer.module.css
index d1ea92630941b65cc9facc26cae4f56ad2b12254..5bfe0e68cf38cffb468ded55522375cdaff62b67 100644
--- a/gatsby/src/components/footer.module.css
+++ b/gatsby/src/components/footer.module.css
@@ -1,6 +1,7 @@
 .footer {
   :global(.sds-footer__content) {
     align-items: center;
+    gap: var(--sds-space-gap-medium);
   }
 
   &__list {
diff --git a/gatsby/src/layouts/global.css b/gatsby/src/layouts/global.css
index 01dee4a1a81854396aebb15262ca0525c386ed83..0bbb66df6571eccf2ccdce3deb9808becc30d49e 100644
--- a/gatsby/src/layouts/global.css
+++ b/gatsby/src/layouts/global.css
@@ -10,6 +10,7 @@
 @import url("@sikt/sds-badge");
 @import url("@sikt/sds-table");
 @import url("@sikt/sds-list");
+@import url("@sikt/sds-toggle");
 
 .sds-sikt-button-group {
   flex-wrap: wrap;
diff --git a/gatsby/src/layouts/index.tsx b/gatsby/src/layouts/index.tsx
index 640f2d9f1ffc8fb94f6a4ac7ffdcf6c8d4a397da..e6a7634d881284b5c12364cf0a0d4b7b6a9d45a8 100644
--- a/gatsby/src/layouts/index.tsx
+++ b/gatsby/src/layouts/index.tsx
@@ -1,4 +1,5 @@
-import { ReactNode } from "react";
+import { useSetColorSchemeFromLocalStorage } from "@sikt/sds-toggle";
+import { ReactNode, useCallback, useState } from "react";
 import Header from "../components/Header";
 import Footer from "../components/Footer";
 import "./global.css";
@@ -6,16 +7,34 @@ import * as style from "./layout.module.css";
 
 interface LayoutProps {
   children: ReactNode;
+  pathname: string;
 }
 
-const Layout = ({ location, children, pageContext }: LayoutProps) => {
+const Layout = ({ pathname, children }: LayoutProps) => {
+  useSetColorSchemeFromLocalStorage();
+
   return (
-    <div className={style.layoutWrapper}>
-      <Header currentHref={location.pathname} />
+    <>
+      <Header currentHref={pathname} />
       <main id="main">{children}</main>
       <Footer className={style.layoutFooter} />
+    </>
+  );
+};
+
+const LayoutWrapper = ({ location, children, pageContext }: LayoutProps) => {
+  const [mounted, setMounted] = useState(false);
+  const divRef = useCallback((node: HTMLDivElement | null) => {
+    if (node) {
+      setMounted(true);
+    }
+  }, []);
+
+  return (
+    <div className={style.layoutWrapper} ref={divRef}>
+      {mounted && <Layout pathname={location.pathname}>{children}</Layout>}
     </div>
   );
 };
 
-export default Layout;
+export default LayoutWrapper;
diff --git a/package-lock.json b/package-lock.json
index ba1597986c5fab30f1b6745d2fe9827235807894..3ae7cfd814f5a2d11f31d7f9e196e44f771a9160 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -79,6 +79,7 @@
         "@sikt/sds-logo": "^2.0.0",
         "@sikt/sds-section": "^3.0.0",
         "@sikt/sds-table": "^2.0.1",
+        "@sikt/sds-toggle": "^2.1.0",
         "@sikt/sds-tokens": "^1.0.0",
         "canvas-confetti": "^1.9.2",
         "clsx": "^2.1.0",
diff --git a/packages/icons/icons.config.mjs b/packages/icons/icons.config.mjs
index 28fcf4b3317da342cdc20ca96f1c17d57b5f4a35..04028d7ab66f606fed7e36e2b5722b86e0ae1700 100644
--- a/packages/icons/icons.config.mjs
+++ b/packages/icons/icons.config.mjs
@@ -46,6 +46,7 @@ export const config = [
   "magnifying-glass",
   "map-pin",
   "megaphone",
+  "moon",
   "minus",
   "minus-circle",
   "paperclip",
@@ -62,6 +63,7 @@ export const config = [
   "sort-ascending",
   "sort-descending",
   "spinner-gap",
+  "sun",
   "trash",
   "upload-simple",
   "user-circle",
diff --git a/packages/toggle/ToggleSwitchColorScheme.test.tsx b/packages/toggle/ToggleSwitchColorScheme.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..111cc14f05faf2deb90866d74b8d0253783d1e0a
--- /dev/null
+++ b/packages/toggle/ToggleSwitchColorScheme.test.tsx
@@ -0,0 +1,463 @@
+import { act, render, renderHook, screen } from "@testing-library/react";
+import {
+  COLOR_SCHEME_LOCAL_STORAGE_KEY,
+  DataColorScheme,
+  getClosestElementForDataColorScheme,
+  getIsDarkModeFromParentTag,
+  setClosestDataColorSchemeValue,
+  ToggleSwitchColorScheme,
+  ToggleSwitchColorSchemeProps,
+  useIsDarkMode,
+  useSetColorSchemeFromLocalStorage,
+} from "./ToggleSwitchColorScheme";
+import { axe } from "jest-axe";
+import userEvent from "@testing-library/user-event";
+import React, { ReactElement } from "react";
+
+/* https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom */
+Object.defineProperty(window, "matchMedia", {
+  writable: true,
+  value: jest.fn().mockImplementation((query) => ({
+    matches: false,
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    media: query,
+    onchange: null,
+    addListener: jest.fn(), // deprecated
+    removeListener: jest.fn(), // deprecated
+    addEventListener: jest.fn(),
+    removeEventListener: jest.fn(),
+    dispatchEvent: jest.fn(),
+  })),
+});
+
+const renderWithMode = (
+  element: ReactElement<
+    ToggleSwitchColorSchemeProps,
+    React.JSXElementConstructor<ToggleSwitchColorSchemeProps>
+  >,
+  mode: string,
+) => {
+  localStorage.setItem(
+    COLOR_SCHEME_LOCAL_STORAGE_KEY,
+    String(mode !== "light"),
+  );
+  return render(element);
+};
+
+const renderWithLightMode = (
+  element: ReactElement<
+    ToggleSwitchColorSchemeProps,
+    React.JSXElementConstructor<ToggleSwitchColorSchemeProps>
+  >,
+) => renderWithMode(element, "light");
+
+const renderWithDarkMode = (
+  element: ReactElement<
+    ToggleSwitchColorSchemeProps,
+    React.JSXElementConstructor<ToggleSwitchColorSchemeProps>
+  >,
+) => renderWithMode(element, "dark");
+
+describe("Toggle Switch - color scheme", () => {
+  beforeEach(() => {
+    localStorage.removeItem(COLOR_SCHEME_LOCAL_STORAGE_KEY);
+  });
+
+  describe("a11y", () => {
+    it("should be accessible without label", async () => {
+      const { container } = renderWithLightMode(
+        <ToggleSwitchColorScheme control="internal" aria-label="aria-label" />,
+      );
+
+      expect(await axe(container)).toHaveNoViolations();
+    });
+
+    it("should be accessible with label", async () => {
+      const { container } = renderWithLightMode(
+        <ToggleSwitchColorScheme control="internal" label="Foo" />,
+      );
+
+      expect(await axe(container)).toHaveNoViolations();
+    });
+  });
+
+  describe("init states", () => {
+    it(`should render toggle switch`, () => {
+      renderWithLightMode(
+        <ToggleSwitchColorScheme
+          control="internal"
+          data-testid="test"
+          label="Foo"
+        />,
+      );
+
+      expect(screen.getByTestId("test")).toHaveClass(
+        "sds-toggle-switch-color-scheme",
+      );
+      expect(screen.getByTestId("test")).toHaveClass(
+        "sds-toggle-switch-color-scheme",
+      );
+      expect(screen.getByTestId("test")).toBeInTheDocument();
+    });
+
+    it("should not be checked when light scheme", () => {
+      renderWithLightMode(
+        <ToggleSwitchColorScheme control="internal" label="Foo" />,
+      );
+
+      expect(screen.getByLabelText("Foo")).not.toBeChecked();
+    });
+
+    it("should be checked when dark scheme", () => {
+      renderWithDarkMode(
+        <ToggleSwitchColorScheme control="internal" label="Foo" />,
+      );
+
+      expect(screen.getByLabelText("Foo")).toBeChecked();
+    });
+
+    it("should render the label after the control", () => {
+      renderWithLightMode(
+        <ToggleSwitchColorScheme
+          control="internal"
+          data-testid="test"
+          label="Foo"
+        />,
+      );
+
+      const container = screen.getByTestId("test");
+      const label = container.getElementsByClassName(
+        "sds-toggle-switch-color-scheme__label-text",
+      )[0];
+      const control = container.getElementsByClassName(
+        "sds-toggle-switch-color-scheme__inner",
+      )[0];
+      expect(label.compareDocumentPosition(control)).toBe(
+        Node.DOCUMENT_POSITION_PRECEDING,
+      );
+    });
+
+    it("should render the label in front of the control", () => {
+      renderWithLightMode(
+        <ToggleSwitchColorScheme
+          control="internal"
+          labelFirst
+          data-testid="test"
+          label="Foo"
+        />,
+      );
+
+      const container = screen.getByTestId("test");
+      const label = container.getElementsByClassName(
+        "sds-toggle-switch-color-scheme__label-text",
+      )[0];
+      const control = container.getElementsByClassName(
+        "sds-toggle-switch-color-scheme__inner",
+      )[0];
+      expect(label.compareDocumentPosition(control)).toBe(
+        Node.DOCUMENT_POSITION_FOLLOWING,
+      );
+    });
+  });
+
+  describe("change", () => {
+    it("calls change handler", async () => {
+      const user = userEvent.setup();
+      const changeHandler = jest.fn();
+      renderWithLightMode(
+        <ToggleSwitchColorScheme
+          control="internal"
+          onChange={changeHandler}
+          label="Foo"
+        />,
+      );
+
+      const label = screen.getByLabelText("Foo");
+      await user.click(label);
+
+      expect(changeHandler).toHaveBeenCalled();
+    });
+  });
+
+  describe("local storage", () => {
+    it("should have no entry on initial render", () => {
+      render(
+        <ToggleSwitchColorScheme
+          control="internal"
+          labelFirst
+          data-testid="test"
+          label="Foo"
+        />,
+      );
+
+      expect(localStorage.getItem(COLOR_SCHEME_LOCAL_STORAGE_KEY)).toBeNull();
+    });
+
+    it("should have correct entry when toggled from light to dark", async () => {
+      const user = userEvent.setup();
+      renderWithLightMode(
+        <ToggleSwitchColorScheme control="internal" label="Foo" />,
+      );
+
+      const label = screen.getByLabelText("Foo");
+      await user.click(label);
+
+      expect(localStorage.getItem(COLOR_SCHEME_LOCAL_STORAGE_KEY)).toBe("true");
+    });
+
+    it("should have correct entry when toggled from dark to light", async () => {
+      const user = userEvent.setup();
+      renderWithDarkMode(
+        <ToggleSwitchColorScheme control="internal" label="Foo" />,
+      );
+
+      const label = screen.getByLabelText("Foo");
+      await user.click(label);
+
+      expect(localStorage.getItem(COLOR_SCHEME_LOCAL_STORAGE_KEY)).toBe(
+        "false",
+      );
+    });
+  });
+
+  describe("external control", () => {
+    afterEach(() => {
+      jest.restoreAllMocks();
+    });
+
+    it("should call changeHandler", async () => {
+      const user = userEvent.setup();
+      const changeHandler = jest.fn();
+      render(
+        <ToggleSwitchColorScheme
+          control="external"
+          isDarkMode={false}
+          onChange={changeHandler}
+          label="Foo"
+        />,
+      );
+
+      const label = screen.getByLabelText("Foo");
+      await user.click(label);
+
+      expect(changeHandler).toHaveBeenCalled();
+    });
+
+    it("should be unchecked when isDarkMode is false", () => {
+      render(
+        <ToggleSwitchColorScheme
+          control="external"
+          onChange={jest.fn}
+          isDarkMode={false}
+          label="Foo"
+        />,
+      );
+
+      expect(screen.getByLabelText("Foo")).not.toBeChecked();
+    });
+
+    it("should be checked when isDarkMode is true", () => {
+      render(
+        <ToggleSwitchColorScheme
+          control="external"
+          onChange={jest.fn}
+          label="Foo"
+          isDarkMode
+        />,
+      );
+
+      expect(screen.getByLabelText("Foo")).toBeChecked();
+    });
+  });
+
+  describe("getClosestElementForDataColorScheme", () => {
+    it("should find closest element with data-color-scheme set", () => {
+      const id = "super-div";
+
+      render(
+        <div data-color-scheme="light" id={id}>
+          <ToggleSwitchColorScheme control="internal" label="Foo" />
+        </div>,
+      );
+
+      expect(
+        getClosestElementForDataColorScheme(screen.getByLabelText("Foo")).id,
+      ).toBe(id);
+    });
+
+    it("should find HTML tag", () => {
+      const id = "super-div";
+
+      render(
+        <div id={id}>
+          <ToggleSwitchColorScheme control="internal" label="Foo" />
+        </div>,
+      );
+
+      expect(
+        getClosestElementForDataColorScheme(screen.getByLabelText("Foo"))
+          .tagName,
+      ).toBe("HTML");
+    });
+  });
+
+  describe("getIsDarkModeFromParentTag", () => {
+    it("should get false from closest parent with data-color-scheme light set", () => {
+      render(
+        <div data-color-scheme="dark">
+          <div data-color-scheme="light">
+            <ToggleSwitchColorScheme control="internal" label="Foo" />
+          </div>
+        </div>,
+      );
+
+      expect(getIsDarkModeFromParentTag(screen.getByLabelText("Foo"))).toBe(
+        false,
+      );
+    });
+
+    it("should get true from closest parent with data-color-scheme dark set", () => {
+      render(
+        <div data-color-scheme="light">
+          <div data-color-scheme="dark">
+            <ToggleSwitchColorScheme control="internal" label="Foo" />
+          </div>
+        </div>,
+      );
+
+      expect(getIsDarkModeFromParentTag(screen.getByLabelText("Foo"))).toBe(
+        true,
+      );
+    });
+
+    it("should get undefined when no parent has data-color-scheme set", () => {
+      render(
+        <div>
+          <ToggleSwitchColorScheme control="internal" label="Foo" />
+        </div>,
+      );
+
+      expect(
+        getIsDarkModeFromParentTag(screen.getByLabelText("Foo")),
+      ).toBeUndefined();
+    });
+  });
+
+  describe("setClosestDataColorSchemeValue", () => {
+    it("should update value of parent div", () => {
+      render(
+        <div data-color-scheme="dark">
+          <ToggleSwitchColorScheme control="internal" label="Foo" />
+        </div>,
+      );
+
+      const element = screen.getByLabelText("Foo");
+
+      expect(getIsDarkModeFromParentTag(element)).toBe(true);
+
+      setClosestDataColorSchemeValue(element, DataColorScheme.LIGHT);
+
+      expect(getIsDarkModeFromParentTag(element)).toBe(false);
+    });
+
+    it("should update value of html tag", () => {
+      render(<ToggleSwitchColorScheme control="internal" label="Foo" />);
+
+      const element = screen.getByLabelText("Foo");
+
+      expect(getIsDarkModeFromParentTag(element)).toBe(undefined);
+
+      setClosestDataColorSchemeValue(element, DataColorScheme.DARK);
+
+      expect(getIsDarkModeFromParentTag(element)).toBe(true);
+
+      expect(document.documentElement.getAttribute("data-color-scheme")).toBe(
+        "dark",
+      );
+    });
+  });
+
+  describe("custom hook isDarkMode", () => {
+    const defaultMatchMedia = window.matchMedia("(prefers-color-scheme: dark)");
+    let windowMatchMediaSpy:
+      | jest.SpyInstance<MediaQueryList, [query: string]>
+      | undefined;
+
+    const setSystemThemeLight = () => {
+      windowMatchMediaSpy?.mockImplementation(() => ({
+        ...defaultMatchMedia,
+        matches: false,
+      }));
+    };
+
+    const setSystemThemeDark = () => {
+      windowMatchMediaSpy?.mockImplementation(() => ({
+        ...defaultMatchMedia,
+        matches: true,
+      }));
+    };
+
+    const getFromLocalStorage = () =>
+      localStorage.getItem(COLOR_SCHEME_LOCAL_STORAGE_KEY);
+
+    beforeEach(() => {
+      windowMatchMediaSpy?.mockRestore();
+      windowMatchMediaSpy = jest.spyOn(window, "matchMedia");
+    });
+
+    it("should be true when system theme is dark", () => {
+      setSystemThemeDark();
+      const { result } = renderHook(useIsDarkMode);
+
+      expect(result.current).toBe(true);
+    });
+
+    it("should be false when system theme is light", () => {
+      setSystemThemeLight();
+      const { result } = renderHook(useIsDarkMode);
+
+      expect(result.current).toBe(false);
+    });
+
+    it("should be able to change value using localStorage", () => {
+      setSystemThemeLight();
+      const { result, rerender } = renderHook(useIsDarkMode);
+
+      expect(getFromLocalStorage()).toBeNull();
+
+      act(() => {
+        localStorage.setItem(COLOR_SCHEME_LOCAL_STORAGE_KEY, String(true));
+      });
+
+      expect(getFromLocalStorage()).toBe("true");
+
+      rerender(); // Seems like the useExternalSync-hook doesn't rerender on storage change in test env
+      expect(result.current).toBe(true);
+    });
+
+    it("should be able to change value of wrapper from value in localStorage", () => {
+      setSystemThemeLight();
+
+      const TestComponent = () => {
+        useSetColorSchemeFromLocalStorage();
+
+        return <body>Hello</body>;
+      };
+
+      const { container, rerender } = render(<TestComponent />, {
+        container: document.documentElement,
+      });
+
+      expect(getFromLocalStorage()).toBeNull();
+
+      act(() => {
+        localStorage.setItem(COLOR_SCHEME_LOCAL_STORAGE_KEY, String(true));
+      });
+
+      expect(getFromLocalStorage()).toBe("true");
+
+      rerender(<TestComponent />); // Seems like the useExternalSync-hook doesn't rerender on storage change in test env
+      expect(container.getAttribute("data-color-scheme")).toBe("dark");
+    });
+  });
+});
diff --git a/packages/toggle/ToggleSwitchColorScheme.tsx b/packages/toggle/ToggleSwitchColorScheme.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4ef7fe4b860a518be4731aa7c9aa199d0c54510f
--- /dev/null
+++ b/packages/toggle/ToggleSwitchColorScheme.tsx
@@ -0,0 +1,377 @@
+import {
+  ChangeEvent,
+  ChangeEventHandler,
+  DetailedHTMLProps,
+  HTMLAttributes,
+  InputHTMLAttributes,
+  ReactNode,
+  useSyncExternalStore,
+} from "react";
+import { MoonIcon, SunIcon } from "@sikt/sds-icons";
+import "./toggle-switch-color-scheme.pcss";
+import clsx from "clsx";
+
+export const COLOR_SCHEME_LOCAL_STORAGE_KEY = "sds-color-scheme-is-dark-mode";
+const COLOR_SCHEME_ATTRIBUTE = "data-color-scheme";
+
+export const DataColorScheme = {
+  LIGHT: "light",
+  DARK: "dark",
+} as const;
+
+type DataColorSchemeType =
+  (typeof DataColorScheme)[keyof typeof DataColorScheme];
+
+export const getClosestElementForDataColorScheme = (
+  element?: HTMLElement,
+  colorSchemeAttribute: string = COLOR_SCHEME_ATTRIBUTE,
+): HTMLElement => {
+  if (!element) return document.documentElement;
+
+  return (
+    element.closest(`[${colorSchemeAttribute}]`) ??
+    element.closest("html") ??
+    document.documentElement
+  );
+};
+
+export const getClosestDataColorSchemeValue = (
+  element: HTMLElement,
+  colorSchemeAttribute: string = COLOR_SCHEME_ATTRIBUTE,
+): string | null | undefined =>
+  getClosestElementForDataColorScheme(element).getAttribute(
+    colorSchemeAttribute,
+  );
+
+export const setClosestDataColorSchemeValue = (
+  element: HTMLElement,
+  value: DataColorSchemeType,
+  colorSchemeAttribute: string = COLOR_SCHEME_ATTRIBUTE,
+) => {
+  getClosestElementForDataColorScheme(element).setAttribute(
+    colorSchemeAttribute,
+    value,
+  );
+};
+
+export const getIsDarkModeFromSystem = (): boolean =>
+  window.matchMedia("(prefers-color-scheme: dark)").matches;
+
+const getIsDarkModeFromLocalStorage = (
+  localStorageKey: string,
+): boolean | undefined => {
+  const fromLocalStorage = localStorage.getItem(localStorageKey);
+
+  return fromLocalStorage ? fromLocalStorage === "true" : undefined;
+};
+
+// Finds and checks the state of the closest parent element with the attribute data-color-scheme set
+export const getIsDarkModeFromParentTag = (
+  wrapperElement: HTMLElement | undefined,
+): boolean | undefined => {
+  if (!wrapperElement) return undefined;
+
+  // Multiple tags might exist e.g. in storybook presentation
+  const fromTag = getClosestDataColorSchemeValue(wrapperElement);
+
+  if (fromTag === DataColorScheme.LIGHT) return false;
+  if (fromTag === DataColorScheme.DARK) return true;
+
+  return undefined;
+};
+
+/*
+ * Checks possible dark mode states in prioritized order and returns the value of the highest defined priority
+ * 1. The value of the closest parent element with data-color-scheme set
+ * 2. The system value
+ * */
+export const getIsDarkModeFromParentTagOrSystem = (
+  parentTagState: boolean | undefined,
+  systemState: boolean,
+): boolean => {
+  if (parentTagState !== undefined) return parentTagState;
+
+  return systemState;
+};
+
+const storageChangeSubscribe = (
+  callback: (this: Window, ev: StorageEvent) => unknown,
+) => {
+  window.addEventListener("storage", callback);
+
+  return () => {
+    window.removeEventListener("storage", callback);
+  };
+};
+
+const matchMediaColorSchemeChangeSubscribe = (
+  callback: (this: MediaQueryList, ev: MediaQueryListEvent) => unknown,
+) => {
+  window
+    .matchMedia("(prefers-color-scheme: dark)")
+    .addEventListener("change", callback);
+
+  return () => {
+    window
+      .matchMedia("(prefers-color-scheme: dark)")
+      .removeEventListener("change", callback);
+  };
+};
+
+const getServerSnapshot = () => false;
+
+interface ColorSchemeHookParams {
+  localStorageKey?: string;
+  wrapperElement?: HTMLElement;
+}
+
+export const useIsDarkMode = ({
+  localStorageKey,
+  wrapperElement,
+}: ColorSchemeHookParams = {}): boolean => {
+  const getLocalStorageSnapshot = () => {
+    const localStorageState = getIsDarkModeFromLocalStorage(
+      localStorageKey ?? COLOR_SCHEME_LOCAL_STORAGE_KEY,
+    );
+
+    if (localStorageState !== undefined) return localStorageState;
+
+    const parentTagState = getIsDarkModeFromParentTag(wrapperElement);
+    const systemState = getIsDarkModeFromSystem();
+
+    return getIsDarkModeFromParentTagOrSystem(parentTagState, systemState);
+  };
+
+  const isDarkModeFromLocalStorage = useSyncExternalStore<boolean | undefined>(
+    storageChangeSubscribe,
+    getLocalStorageSnapshot,
+    getServerSnapshot,
+  );
+
+  const getMatchMediaColorSchemeChangeSnapshot = () =>
+    window.matchMedia("(prefers-color-scheme: dark)").matches;
+
+  const isDarkModeFromSystemChange = useSyncExternalStore<boolean>(
+    matchMediaColorSchemeChangeSubscribe,
+    getMatchMediaColorSchemeChangeSnapshot,
+    getServerSnapshot,
+  );
+
+  return isDarkModeFromLocalStorage ?? isDarkModeFromSystemChange;
+};
+
+export const useSetColorSchemeFromLocalStorage = ({
+  localStorageKey,
+  wrapperElement = document.documentElement,
+}: ColorSchemeHookParams = {}) => {
+  const isDarkMode = useIsDarkMode({ localStorageKey, wrapperElement });
+
+  setClosestDataColorSchemeValue(
+    wrapperElement,
+    isDarkMode ? DataColorScheme.DARK : DataColorScheme.LIGHT,
+  );
+};
+
+type ToggleSwitchHTMLProps = Omit<
+  DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>,
+  "onChange" | "aria-label"
+>;
+
+interface Label {
+  /**
+   * A custom label. One must either pass in a `label` or an `aria-label` to maintain accessibility.
+   * */
+  label: ReactNode;
+  /**
+   * A custom aria-label. One must either pass in a `label` or an `aria-label` to maintain accessibility.
+   * */
+  "aria-label"?: never;
+}
+
+interface AriaLabel {
+  /**
+   * A custom aria-label. One must either pass in a `label` or an `aria-label` to maintain accessibility.
+   * */
+  "aria-label": string;
+  /**
+   * A custom label. One must either pass in a `label` or an `aria-label` to maintain accessibility.
+   * */
+  label?: never;
+}
+
+type LabelOrAriaLabel = Label | AriaLabel;
+
+interface ExternalControl {
+  /**
+   * Required prop defining whether the dark mode toggle state is set to be controlled externally or internally.
+   * */
+  control: "external";
+  /**
+   * Sets the checked value of the toggle switch.
+   * */
+  isDarkMode: boolean;
+  /**
+   * Required to handle change event, when control is external.
+   * */
+  onChange: ChangeEventHandler<HTMLInputElement>;
+}
+
+interface InternalControl {
+  /**
+   * Required prop defining whether the dark mode toggle state is set to be controlled externally or internally.
+   * */
+  control: "internal";
+  isDarkMode?: never;
+  onChange?: ChangeEventHandler<HTMLInputElement>;
+}
+
+type Control = ExternalControl | InternalControl;
+
+export type ToggleSwitchColorSchemeProps = ToggleSwitchHTMLProps &
+  LabelOrAriaLabel &
+  Control & {
+    /**
+     * Whether the `label`, if it exists, should be placed in front of the component.
+     * */
+    labelFirst?: boolean;
+    /**
+     * Props sent directly to the wrapped input component.
+     * */
+    inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, "aria-label">;
+    /**
+     * A custom key for local storage to use, instead of the default `sds-color-scheme`.
+     * Needed e.g. when using multiple `ToggleSwitchColorScheme` components and dark mode sections.
+     * */
+    localStorageKey?: string;
+  };
+
+type ToggleSwitchColorSchemeInnerProps = Pick<
+  ToggleSwitchColorSchemeProps,
+  keyof ToggleSwitchHTMLProps | "label" | "inputProps" | "aria-label"
+> &
+  Pick<
+    Required<ToggleSwitchColorSchemeProps>,
+    "isDarkMode" | "onChange" | "labelFirst"
+  >;
+
+const ToggleSwitchColorSchemeInner = ({
+  className,
+  label,
+  labelFirst,
+  onChange,
+  isDarkMode,
+  ...props
+}: ToggleSwitchColorSchemeInnerProps) => {
+  const labelElement = label && (
+    <div className="sds-toggle-switch-color-scheme__label-text">{label}</div>
+  );
+
+  return (
+    <div
+      {...props}
+      className={clsx(
+        "sds-toggle-switch-color-scheme",
+        isDarkMode && "sds-toggle-switch-color-scheme--checked",
+        className,
+      )}
+    >
+      <label className="sds-toggle-switch-color-scheme__label">
+        {labelFirst && labelElement}
+        <div className="sds-toggle-switch-color-scheme__inner">
+          <input
+            type="checkbox"
+            role="switch"
+            className="sds-toggle-switch-color-scheme__track"
+            checked={isDarkMode}
+            onChange={onChange}
+            readOnly={!onChange}
+            {...{
+              ...props.inputProps,
+              "aria-label": props["aria-label"],
+            }}
+          />
+          <div className="sds-toggle-switch-color-scheme__thumb">
+            {isDarkMode ? (
+              <MoonIcon className="sds-toggle-switch-color-scheme__icon" />
+            ) : (
+              <SunIcon className="sds-toggle-switch-color-scheme__icon" />
+            )}
+          </div>
+        </div>
+        {!labelFirst && labelElement}
+      </label>
+    </div>
+  );
+};
+
+type ToggleSwitchColorSchemeInternalControlProps = Pick<
+  ToggleSwitchColorSchemeProps,
+  | keyof ToggleSwitchHTMLProps
+  | "label"
+  | "inputProps"
+  | "localStorageKey"
+  | "onChange"
+  | "aria-label"
+> &
+  Pick<Required<ToggleSwitchColorSchemeProps>, "labelFirst">;
+
+const ToggleSwitchColorSchemeInternalControl = ({
+  localStorageKey,
+  onChange: externalOnChange,
+  ...props
+}: ToggleSwitchColorSchemeInternalControlProps) => {
+  const isDarkMode = useIsDarkMode({ localStorageKey });
+
+  const onChange = (event: ChangeEvent<HTMLInputElement>) => {
+    localStorage.setItem(
+      localStorageKey ?? COLOR_SCHEME_LOCAL_STORAGE_KEY,
+      String(event.currentTarget.checked),
+    );
+    window.dispatchEvent(new Event("storage"));
+
+    if (externalOnChange) externalOnChange(event);
+  };
+
+  return (
+    <ToggleSwitchColorSchemeInner
+      {...props}
+      isDarkMode={isDarkMode}
+      onChange={onChange}
+    />
+  );
+};
+
+/**
+ * A component that makes it possible to toggle dark mode on/off in *@sikt/sds-* contexts.
+ *
+ * When `control` is set to `external`, all state management of the toggle component is handled by the `isDarkMode`
+ * and `onChange` props. This is necessary e.g. when using SSR rendering (NextJS).
+ *
+ * When `control` is set to `internal`, state management is handled internally in the component, and `localStorage` is
+ * used to keep track of state.
+ * Toggling dark mode from the value in `localStorage` can be done by the custom hook `useSetColorSchemeFromLocalStorage`.
+ * Examples of this are shown in the "Show code" sections of the stories.
+ * */
+export const ToggleSwitchColorScheme = ({
+  localStorageKey = COLOR_SCHEME_LOCAL_STORAGE_KEY,
+  labelFirst = false,
+  control,
+  onChange,
+  isDarkMode,
+  ...props
+}: ToggleSwitchColorSchemeProps) =>
+  control === "external" ? (
+    <ToggleSwitchColorSchemeInner
+      {...props}
+      onChange={onChange}
+      labelFirst={labelFirst}
+      isDarkMode={isDarkMode}
+    />
+  ) : (
+    <ToggleSwitchColorSchemeInternalControl
+      {...props}
+      localStorageKey={localStorageKey}
+      labelFirst={labelFirst}
+      onChange={onChange}
+    />
+  );
diff --git a/packages/toggle/index.ts b/packages/toggle/index.ts
index 44f83be279cd23622493b205038c9be3d3694585..782ac830ccb3a7e3f1f6451e4fe221bf91ddb9a7 100644
--- a/packages/toggle/index.ts
+++ b/packages/toggle/index.ts
@@ -6,3 +6,8 @@ export { ToggleSegment } from "./ToggleSegment";
 export { ToggleSegmentOption } from "./ToggleSegmentOption";
 export type { ToggleSegmentProps } from "./ToggleSegment";
 export type { ToggleSegmentOptionProps } from "./ToggleSegmentOption";
+export type { ToggleSwitchColorSchemeProps } from "./ToggleSwitchColorScheme";
+export {
+  ToggleSwitchColorScheme,
+  useSetColorSchemeFromLocalStorage,
+} from "./ToggleSwitchColorScheme";
diff --git a/packages/toggle/stories/ToggleSwitchColorScheme.stories.tsx b/packages/toggle/stories/ToggleSwitchColorScheme.stories.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4ecfb3e7a2e568386a156f6b240690eed112f878
--- /dev/null
+++ b/packages/toggle/stories/ToggleSwitchColorScheme.stories.tsx
@@ -0,0 +1,274 @@
+import { Meta, StoryObj } from "@storybook/react";
+import {
+  ToggleSwitchColorScheme as ToggleSwitchColorSchemeToMask,
+  ToggleSwitchColorSchemeProps,
+} from "../index";
+import { ChangeEvent, SyntheticEvent, useCallback, useState } from "react";
+import {
+  getIsDarkModeFromParentTag,
+  getIsDarkModeFromParentTagOrSystem,
+  getIsDarkModeFromSystem,
+} from "../ToggleSwitchColorScheme";
+
+const meta: Meta = {
+  title: "Components/Toggle/ToggleSwitchColorScheme",
+  component: ToggleSwitchColorSchemeToMask,
+};
+
+export default meta;
+
+type Story = StoryObj<ToggleSwitchColorSchemeProps>;
+
+const setValueOnClosestSBAnchor = (element: HTMLElement, value: boolean) => {
+  element
+    .closest(".sb-anchor")
+    ?.setAttribute("data-color-scheme", value ? "dark" : "light");
+};
+
+// Mask default ToggleSwitchColorScheme internal implementation to work around storybook wrapper limitations,
+// and make "Show code" have pretty presentation
+const ToggleSwitchColorScheme = ({ ...args }: ToggleSwitchColorSchemeProps) => {
+  const [isDarkMode, setIsDarkMode] = useState(
+    args.isDarkMode ??
+      getIsDarkModeFromParentTagOrSystem(
+        getIsDarkModeFromParentTag(undefined),
+        getIsDarkModeFromSystem(),
+      ),
+  );
+
+  const onChange = (event: ChangeEvent<HTMLInputElement>) => {
+    const { checked } = event.currentTarget;
+
+    setIsDarkMode(checked);
+    setValueOnClosestSBAnchor(event.currentTarget, checked);
+    if (args.onChange) args.onChange(event);
+  };
+
+  const ref = useCallback((node: HTMLSpanElement | null) => {
+    if (node) {
+      const valueFromContext =
+        args.isDarkMode ??
+        getIsDarkModeFromParentTagOrSystem(
+          getIsDarkModeFromParentTag(node),
+          getIsDarkModeFromSystem(),
+        );
+      setIsDarkMode(valueFromContext);
+      setValueOnClosestSBAnchor(node, valueFromContext);
+    }
+  }, []);
+
+  return (
+    <span ref={ref}>
+      <ToggleSwitchColorSchemeToMask
+        {...args}
+        onChange={onChange}
+        isDarkMode={isDarkMode}
+        control="external"
+      />
+    </span>
+  );
+};
+
+const hookPrefix = `
+// Above in component hierarchy (not compatible with control set to "external")
+useSetColorSchemeFromLocalStorage();
+// or
+useSetColorSchemeFromLocalStorage({ wrapperElement: someElement, localStorageKey: someKey });
+`;
+
+const ariaLabel = "Toggle switch color scheme";
+
+const defaultCode = `
+${hookPrefix}
+<ToggleSwitchColorScheme control="internal" aria-label="${ariaLabel}" />`;
+
+export const Default: Story = {
+  render: (args: ToggleSwitchColorSchemeProps) => (
+    <ToggleSwitchColorScheme {...args} />
+  ),
+  args: {
+    control: "internal",
+    "aria-label": ariaLabel,
+  },
+  parameters: {
+    docs: {
+      source: {
+        language: "tsx",
+        code: defaultCode,
+      },
+    },
+  },
+};
+
+const withLabelCode = `
+${hookPrefix}
+<ToggleSwitchColorScheme
+  control="internal"
+  label="Label"
+/>`;
+
+export const WithLabel: Story = {
+  render: (args: ToggleSwitchColorSchemeProps) => (
+    <ToggleSwitchColorScheme {...args} />
+  ),
+  args: {
+    control: "internal",
+    label: "Label",
+  },
+  parameters: {
+    docs: {
+      source: {
+        language: "tsx",
+        code: withLabelCode,
+      },
+    },
+  },
+};
+
+const withLabelFirstCode = `
+${hookPrefix}
+<ToggleSwitchColorScheme
+  control="internal"
+  label="Label"
+  labelFirst
+/>`;
+
+export const WithLabelFirst: Story = {
+  render: (args: ToggleSwitchColorSchemeProps) => (
+    <ToggleSwitchColorScheme {...args} />
+  ),
+  args: {
+    control: "internal",
+    label: "Label",
+    labelFirst: true,
+  },
+  parameters: {
+    docs: {
+      source: {
+        language: "tsx",
+        code: withLabelFirstCode,
+      },
+    },
+  },
+};
+
+const externalCode = `
+const MyComponent = () => {
+  const [isDarkMode, setIsDarkMode] = useState(false);
+
+  return (
+    <ToggleSwitchColorScheme
+      control="external"
+      isDarkMode={isDarkMode}
+      onChange={({ currentTarget: { checked } }) => {
+        alert(\`Color scheme is updated to: \${checked ? "dark" : "light"}\`);
+        setIsDarkMode(checked);
+      }}
+      aria-label="${ariaLabel}"
+    />
+  );
+}
+`;
+
+export const ExternalControl: Story = {
+  render: (args: ToggleSwitchColorSchemeProps) => (
+    <ToggleSwitchColorScheme {...args} />
+  ),
+  args: {
+    control: "external",
+    isDarkMode: false,
+    onChange: (event: SyntheticEvent<HTMLInputElement>) => {
+      alert(
+        `Color scheme is updated to: ${
+          event.currentTarget.checked ? "dark" : "light"
+        }`,
+      );
+    },
+    "aria-label": ariaLabel,
+  },
+  parameters: {
+    docs: {
+      source: {
+        language: "tsx",
+        code: externalCode,
+      },
+    },
+  },
+};
+
+const nextCode = `
+// app/i-am-root.tsx (where html tag is defined)
+import { cookies } from "next/headers";
+
+const initColorScheme = cookies().get("data-color-scheme")?.value;
+const App = () => <html lang="en" data-color-scheme={initColorScheme ?? "light"}>{rest-of-app}</html>;
+
+// lib/colorSchemeServerActions.ts
+"use server"; // Stable as of NextJS v14 - experimental in NextJS v13 (needs to be enabled in next.config.js)
+
+import { cookies } from "next/headers";
+
+export const getCookieValueFromServer = async () =>
+  cookies().get("data-color-scheme")?.value === "dark";
+
+export const setCookieValueInServerContext = async (value: string) => {
+  cookies().set(COOKIE_KEY, value);
+};
+
+// lib/toggleSwitchColorSchemeClient.tsx
+"use client";
+
+import { ToggleSwitchColorScheme as SDSToggleSwitchColorScheme } from "@sikt/sds-toggle";
+import { getCookieValueFromServer, setCookieInServerContext } from "@/src/lib/colorSchemeServerActions";
+
+const ToggleSwitchColorSchemeClient = () => {
+  const [isDarkMode, setIsDarkMode] = useState<boolean>();
+
+  useEffect(() => {
+    startTransition(() => {
+      getCookieValueFromServer().then((res) => setIsDarkMode(res));
+    });
+  }, []);
+
+  const onChangeHandler = ({
+    currentTarget: { checked },
+  }: ChangeEvent<HTMLInputElement>) => {
+    startTransition(() => {
+      setCookieValueInServerContext(checked ? "dark" : "light").then(() => {
+        setIsDarkMode(checked);
+      });
+    });
+  };
+
+  return isDarkMode !== undefined ? (
+    <SDSToggleSwitchColorScheme
+      control="external"
+      isDarkMode={isDarkMode}
+      onChange={onChangeHandler}
+      aria-label="${ariaLabel}"
+    />
+  ) : <></>;
+};
+
+export default ToggleSwitchColorSchemeClient;
+
+// compopnent/that/needs/ToggleSwitchColorScheme.tsx
+import ToggleSwitchColorScheme from 'lib/toggleSwitchColorSchemeClient';
+
+<ToggleSwitchColorScheme />
+`;
+
+export const NextJSExampleImplementation: StoryObj = {
+  name: "Pseudo code of NextJS implementation (using cookies)",
+  parameters: {
+    docs: {
+      canvas: {
+        sourceState: "shown",
+      },
+      source: {
+        language: "tsx",
+        code: nextCode,
+      },
+    },
+  },
+};
diff --git a/packages/toggle/toggle-switch-color-scheme.pcss b/packages/toggle/toggle-switch-color-scheme.pcss
new file mode 100644
index 0000000000000000000000000000000000000000..d7dd1cf5c738d2f77be25db1b94f5362f8abd502
--- /dev/null
+++ b/packages/toggle/toggle-switch-color-scheme.pcss
@@ -0,0 +1,151 @@
+.sds-toggle-switch-color-scheme {
+  --toggle-transition-duration: var(--sds-effect-animation-duration-medium);
+  --toggle-track-width: var(--sds-base-size-l);
+  --toggle-thumb-size: var(--sds-base-size-s1);
+  --toggle-border-width: var(--sds-space-border-weight-regular);
+  --toggle-padding: var(--sds-space-padding-minimal);
+  --toggle-thumb-offset: var(--toggle-padding);
+  --toggle-track-background-color: color-mix(
+    in srgb,
+    white,
+    var(--sds-color-support-warning-default)
+  );
+  --toggle-track-border-color: var(
+    --sds-color-interaction-neutral-strong-default
+  );
+  --toggle-thumb-background-color: var(
+    --sds-color-interaction-neutral-strong-default
+  );
+  --toggle-thumb-color: white;
+  --toggle-thumb-position: 0;
+
+  &__label {
+    align-items: center;
+    cursor: pointer;
+    display: inline-flex;
+    gap: var(--sds-space-padding-small);
+  }
+
+  &:hover,
+  &:active {
+    --toggle-track-background-color: color-mix(
+      in srgb,
+      white 70%,
+      var(--sds-color-support-warning-default)
+    );
+  }
+
+  &:hover {
+    --toggle-track-border-color: var(
+      --sds-color-interaction-neutral-strong-highlight
+    );
+    --toggle-thumb-background-color: var(
+      --sds-color-interaction-neutral-strong-highlight
+    );
+  }
+
+  &:active {
+    --toggle-track-border-color: var(
+      --sds-color-interaction-neutral-strong-pressed
+    );
+    --toggle-thumb-background-color: var(
+      --sds-color-interaction-neutral-strong-pressed
+    );
+  }
+
+  &--checked {
+    --toggle-track-background-color: var(
+      --sds-color-interaction-primary-strong-default
+    );
+    --toggle-track-border-color: var(--toggle-track-background-color);
+    --toggle-thumb-background-color: var(--sds-color-layout-background-default);
+    --toggle-thumb-position: calc(
+      var(--toggle-track-width) - var(--toggle-thumb-size) - 2 *
+        var(--toggle-thumb-offset)
+    );
+
+    &:hover,
+    &:active {
+      --toggle-thumb-background-color: var(
+        --sds-color-layout-background-default
+      );
+    }
+
+    &:hover {
+      --toggle-track-background-color: var(
+        --sds-color-interaction-primary-strong-highlight
+      );
+      --toggle-track-border-color: var(
+        --sds-color-interaction-primary-strong-highlight
+      );
+    }
+
+    &:active {
+      --toggle-track-background-color: var(
+        --sds-color-interaction-primary-strong-pressed
+      );
+      --toggle-track-border-color: var(
+        --sds-color-interaction-primary-strong-pressed
+      );
+    }
+  }
+
+  &__inner {
+    align-items: center;
+    display: inline-flex;
+    padding: calc(var(--sds-space-padding-small) - var(--toggle-border-width)) 0;
+    position: relative;
+  }
+
+  &__label-text {
+    align-items: center;
+    color: var(--sds-color-text-primary);
+    display: flex;
+    font-size: var(--sds-typography-body-fontsize-regular);
+    font-weight: var(--sds-typography-weight-regular);
+    line-height: var(--sds-typography-body-lineheight-regular);
+    padding: var(--sds-space-padding-tiny);
+  }
+
+  &__track {
+    appearance: none;
+    cursor: pointer;
+    background-color: var(--toggle-track-background-color);
+    border: var(--toggle-border-width) solid var(--toggle-track-border-color);
+    border-radius: var(--toggle-thumb-size);
+    height: calc(
+      var(--toggle-thumb-size) + calc(2 * var(--toggle-thumb-offset))
+    );
+    width: var(--toggle-track-width);
+    padding: var(--toggle-padding);
+    transition:
+      background-color var(--toggle-transition-duration),
+      border-color var(--toggle-transition-duration);
+
+    &:focus-visible {
+      outline: var(--sds-focus-outline);
+      outline-offset: 0;
+    }
+  }
+
+  &__thumb {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    position: absolute;
+    left: var(--toggle-thumb-offset);
+    height: var(--toggle-thumb-size);
+    width: var(--toggle-thumb-size);
+    border-radius: var(--sds-space-border-radius-full);
+    transition: all var(--toggle-transition-duration);
+    background-color: var(--toggle-thumb-background-color);
+    transform: translateX(var(--toggle-thumb-position));
+    padding: var(--sds-space-border-weight-regular);
+
+    & > .sds-toggle-switch-color-scheme__icon {
+      transition: all var(--toggle-transition-duration);
+      color: var(--toggle-thumb-color);
+      font-size: var(--sds-base-size-s);
+    }
+  }
+}