diff --git a/eslint.config.js b/eslint.config.js
index 68b0b81ed9..bb24d09026 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -38,6 +38,7 @@ module.exports = [
HTMLButtonElement: "readonly",
HTMLDivElement: "readonly",
HTMLLIElement: "readonly",
+ HTMLImageElement: "readonly",
MouseEvent: "readonly",
KeyboardEvent: "readonly",
Node: "readonly",
diff --git a/src/components/Common/CardImage.tsx b/src/components/Common/CardImage.tsx
deleted file mode 100644
index 98557c7c35..0000000000
--- a/src/components/Common/CardImage.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from "react";
-import ThemedImage from "@theme/ThemedImage";
-
-export interface CardImageProps {
- imgSrc: string | { light: string; dark: string };
- imgAlt?: string;
- className?: string;
-}
-
-export default function CardImage({
- imgSrc,
- imgAlt = "",
- className = "",
-}: CardImageProps) {
- const isThemedImage = typeof imgSrc === "object" && imgSrc !== null;
-
- if (isThemedImage) {
- return (
-
- );
- }
-
- return (
-
- );
-}
-
diff --git a/src/components/Common/CardWithImage.tsx b/src/components/Common/CardWithImage.tsx
index 838980d6e4..5c1dfb89e7 100644
--- a/src/components/Common/CardWithImage.tsx
+++ b/src/components/Common/CardWithImage.tsx
@@ -4,7 +4,7 @@ import Heading from "@theme/Heading";
import { Icon } from "@site/src/components/Common/Icon";
import { IconName } from "@site/src/typescript/iconName";
import Badge from "@site/src/components/Common/Badge";
-import CardImage from "@site/src/components/Common/CardImage";
+import LazyImage from "@site/src/components/Common/LazyImage";
import isInternalUrl from "@docusaurus/isInternalUrl";
import clsx from "clsx";
import { useTagLimit } from "@site/src/hooks/useTagLimit";
@@ -65,15 +65,14 @@ export default function CardWithImage({
"flex items-center justify-center",
"rounded-xl mb-4 overflow-hidden",
"relative aspect-[79/24]",
- hasImage
- ? "bg-black/40"
- : "bg-gradient-to-b from-[#204879] to-[#0F1425] to-[70%]",
+ !hasImage &&
+ "bg-gradient-to-b from-[#204879] to-[#0F1425] to-[70%]",
)}
>
{hasImage ? (
-
- {
+ imgSrc?: string | { light: string; dark: string };
+}
+
+export default function LazyImage({
+ imgSrc,
+ src,
+ alt = "",
+ className,
+ style,
+ ...props
+}: LazyImageProps) {
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ const handleLoaded = () => {
+ setIsLoaded(true);
+ };
+
+ const sources = getSources({ imgSrc, src });
+
+ return (
+
+ {!isLoaded && (
+
+ )}
+
+
+ );
+}
+
+function getSources({
+ imgSrc,
+ src,
+}: Pick): ThemedImageProps["sources"] {
+ if (src) {
+ return {
+ light: src,
+ dark: src,
+ };
+ }
+
+ if (typeof imgSrc === "string") {
+ return {
+ light: imgSrc,
+ dark: imgSrc,
+ };
+ }
+
+ return imgSrc;
+}
diff --git a/src/css/custom.css b/src/css/custom.css
index 891f0c1143..0baacf1210 100644
--- a/src/css/custom.css
+++ b/src/css/custom.css
@@ -2,6 +2,14 @@
@custom-variant dark (&:is([data-theme="dark"] *));
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation: none !important;
+ transition: none !important;
+ scroll-behavior: auto !important;
+ }
+}
+
/* Custom animations for layout switcher */
@keyframes slide-in-from-bottom {
from {
@@ -575,3 +583,36 @@ a {
.theme-doc-sidebar-item-category-level-1 > .menu__list {
@apply !m-0 !p-0 !border-0;
}
+
+/* Skeleton loading animation */
+@keyframes skeleton-loading {
+ 0% {
+ background-position: 150% 0;
+ }
+ 100% {
+ background-position: -150% 0;
+ }
+}
+
+.skeleton {
+ @apply !bg-black/10 dark:!bg-white/5;
+ color: transparent !important;
+ background-image: linear-gradient(
+ 45deg,
+ color-mix(in oklab, var(--color-black) 7.5%, transparent) 40%,
+ color-mix(in oklab, var(--color-black) 0.5%, transparent) 50%,
+ color-mix(in oklab, var(--color-black) 7.5%, transparent) 60%
+ );
+ background-size: 350% 100%;
+ border-radius: var(--radius-xl);
+ animation: skeleton-loading 2000ms infinite linear;
+}
+
+[data-theme="dark"] .skeleton {
+ background-image: linear-gradient(
+ 45deg,
+ color-mix(in oklab, var(--color-white) 0.5%, transparent) 40%,
+ color-mix(in oklab, var(--color-white) 7.5%, transparent) 50%,
+ color-mix(in oklab, var(--color-white) 0.5%, transparent) 60%
+ );
+}
diff --git a/src/theme/MDXComponents/MDXImg.tsx b/src/theme/MDXComponents/MDXImg.tsx
new file mode 100644
index 0000000000..01537fb691
--- /dev/null
+++ b/src/theme/MDXComponents/MDXImg.tsx
@@ -0,0 +1,6 @@
+import React, { ComponentProps } from "react";
+import LazyImage from "@site/src/components/Common/LazyImage";
+
+export default function MDXImg(props: ComponentProps) {
+ return ;
+}
diff --git a/src/theme/MDXComponents/index.tsx b/src/theme/MDXComponents/index.tsx
new file mode 100644
index 0000000000..99f4863d68
--- /dev/null
+++ b/src/theme/MDXComponents/index.tsx
@@ -0,0 +1,7 @@
+import MDXComponents from "@theme-original/MDXComponents";
+import MDXImg from "./MDXImg";
+
+export default {
+ ...MDXComponents,
+ img: MDXImg,
+} satisfies typeof MDXComponents;