fix(widgets): eliminate layout shift with fallback dimensions

Provide estimated width/height to widgets before useElementSize
measures the DOM, preventing responsive breakpoint shifts during
hydration. Replace the Loader spinner with a Skeleton for widget
loading states.
This commit is contained in:
Thomas Camlong committed 2026-05-22 11:37:51 +02:00
1 parent f53527e2e7
commit 10cedbd906
2 files changed
+10 -7

No files matched your search

@@ -27,10 +27,17 @@ const getOverflowFromKind = (kind: SectionItem["kind"]) => {
return undefined; return undefined;
}; };
const ASSUMED_VIEWPORT_WIDTH = 900;
const DEFAULT_COLUMN_COUNT = 12;
export const BoardItemContent = ({ item }: BoardItemContentProps) => { export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const { ref, width, height } = useElementSize<HTMLDivElement>(); const { ref, width, height } = useElementSize<HTMLDivElement>();
const board = useRequiredBoard(); const board = useRequiredBoard();
const estimatedCellSize = ASSUMED_VIEWPORT_WIDTH / DEFAULT_COLUMN_COUNT;
const fallbackWidth = item.width * estimatedCellSize - 20;
const fallbackHeight = item.height * estimatedCellSize - 20;
return ( return (
<> <>
<Card <Card
@@ -53,7 +60,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
}} }}
p={0} p={0}
> >
<InnerContent item={item} width={width} height={height} /> <InnerContent item={item} width={width || fallbackWidth} height={height || fallbackHeight} />
</Card> </Card>
{item.advancedOptions.title?.trim() && ( {item.advancedOptions.title?.trim() && (
<Badge <Badge
+2 -6
View File
@@ -1,7 +1,7 @@
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import type { Loader } from "next/dynamic"; import type { Loader } from "next/dynamic";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Center, Loader as UiLoader } from "@mantine/core"; import { Skeleton } from "@mantine/core";
import { objectEntries } from "@homarr/common"; import { objectEntries } from "@homarr/common";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
@@ -107,11 +107,7 @@ export const loadWidgetDynamic = <TKind extends WidgetKind>(kind: TKind) => {
const newlyLoadedComponent = dynamic<WidgetComponentProps<TKind>>( const newlyLoadedComponent = dynamic<WidgetComponentProps<TKind>>(
widgetImports[kind].componentLoader as Loader<WidgetComponentProps<TKind>>, widgetImports[kind].componentLoader as Loader<WidgetComponentProps<TKind>>,
{ {
loading: () => ( loading: () => <Skeleton w="100%" h="100%" radius="md" animate />,
<Center w="100%" h="100%">
<UiLoader />
</Center>
),
}, },
); );