Skip to content

Commit e83e830

Browse files
leopoldblumclaude
andcommitted
Fix img-better carousel slides cropping at top
The page-level `.prose-content .img-better { margin: 1.25rem 0 }` rule in PostLayout shifted the absolute-positioned wrapper off-center, which combined with overflow:hidden produced visible top-clipping on portrait slides. Compute exact pixel dimensions in JS and zero the inherited margin instead of relying on CSS auto + max-* + object-fit:contain. The 8px inset on each side leaves room for the wrapper's hover:ring-2 to render without being clipped by the slides container. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6e48e3f commit e83e830

1 file changed

Lines changed: 47 additions & 24 deletions

File tree

src/components/mdx/Carousel.tsx

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -81,50 +81,73 @@ export default function Carousel({
8181
}
8282

8383
if (slide.classList.contains('img-better')) {
84-
// Direct <Img> as slide: center naturally, don't fill container
84+
// Direct <Img> as slide: shrink-wrap to image at correct aspect ratio.
85+
// We explicitly compute image dimensions in JS rather than relying on
86+
// CSS auto + max-* + object-fit:contain, because the surrounding page
87+
// CSS (e.g. `.prose-content .img-better { margin: 1.25rem 0 }` in
88+
// PostLayout) and the wrapper's inline aspect-ratio interact with the
89+
// absolute positioning to produce visible top-cropping.
90+
const img = slide.querySelector('img');
8591
if (!isAutoHeight) {
86-
// Clear inset so it doesn't stretch to fill
8792
slide.style.inset = '';
8893
slide.style.top = '50%';
8994
slide.style.left = '50%';
95+
slide.style.margin = '0';
9096
slide.style.transform = isActive
9197
? 'translate(-50%, -50%) scale(1) translateY(0)'
9298
: 'translate(-50%, -50%) scale(0.97) translateY(4px)';
99+
100+
if (img) {
101+
const container = containerRef.current;
102+
// Leave room for the wrapper's hover:ring-2 (2px) plus breathing
103+
// room, so the ring isn't clipped by the slides container's
104+
// overflow:hidden when it appears on hover.
105+
const RING_INSET = 8;
106+
const cw = Math.max(0, (container?.clientWidth || 0) - RING_INSET * 2);
107+
const ch = Math.max(0, (container?.clientHeight || 0) - RING_INSET * 2);
108+
const naturalW = parseInt(img.getAttribute('width') || '0', 10) || img.naturalWidth || 1;
109+
const naturalH = parseInt(img.getAttribute('height') || '0', 10) || img.naturalHeight || 1;
110+
const ratio = naturalW / naturalH;
111+
let displayW: number;
112+
let displayH: number;
113+
if (cw / ch > ratio) {
114+
displayH = ch;
115+
displayW = ch * ratio;
116+
} else {
117+
displayW = cw;
118+
displayH = cw / ratio;
119+
}
120+
img.style.width = `${displayW}px`;
121+
img.style.height = `${displayH}px`;
122+
img.style.maxWidth = '';
123+
img.style.maxHeight = '';
124+
img.style.objectFit = '';
125+
slide.style.width = `${displayW}px`;
126+
slide.style.height = `${displayH}px`;
127+
}
93128
} else {
94129
slide.style.transform = isActive ? 'scale(1) translateY(0)' : 'scale(0.97) translateY(4px)';
130+
slide.style.width = 'auto';
131+
slide.style.height = 'auto';
132+
slide.style.margin = '';
133+
if (img) {
134+
img.style.maxWidth = '100%';
135+
img.style.maxHeight = '';
136+
img.style.width = '';
137+
img.style.height = '';
138+
img.style.objectFit = '';
139+
}
95140
}
96-
slide.style.width = 'auto';
97-
slide.style.height = 'auto';
98141
slide.style.maxWidth = '100%';
99142
slide.style.maxHeight = isAutoHeight ? '' : '100%';
100143
slide.style.aspectRatio = 'unset';
101-
// Don't need flex centering since the div wraps the image tightly
102144
slide.style.display = '';
103145
slide.style.alignItems = '';
104146
slide.style.justifyContent = '';
105147
slide.style.overflow = '';
106148
slide.style.transition = isActive
107149
? `opacity 420ms ${EASE}, transform 420ms ${EASE}, box-shadow 300ms ${EASE}, visibility 0s 0s`
108150
: `opacity 420ms ${EASE}, transform 420ms ${EASE}, box-shadow 300ms ${EASE}, visibility 0s 420ms`;
109-
const img = slide.querySelector('img');
110-
if (img) {
111-
if (isAutoHeight) {
112-
img.style.maxWidth = '100%';
113-
img.style.maxHeight = '';
114-
img.style.width = '';
115-
img.style.height = '';
116-
img.style.objectFit = '';
117-
} else {
118-
const container = containerRef.current;
119-
const cw = container?.clientWidth || 0;
120-
const ch = container?.clientHeight || 0;
121-
img.style.maxWidth = `${cw}px`;
122-
img.style.maxHeight = `${ch - 16}px`;
123-
img.style.width = 'auto';
124-
img.style.height = 'auto';
125-
img.style.objectFit = 'contain';
126-
}
127-
}
128151
} else {
129152
// HTML content or wrapped images: center naturally
130153
slide.querySelectorAll<HTMLElement>('.img-better').forEach((el) => {

0 commit comments

Comments
 (0)