Hover a column to stretch it, click one to make it active, or use the arrows. Clicking the revealed card opens its link, just like the button. Resize below 640px to see the scroll-snap mobile layout. Photos by Unsplash.
See the latest from ABUI.
Our annual letter explores the trends defining the component economy, including faster adoption of registries, design tokens everywhere, and agentic UI generation.
Pass activeIndex and onActiveIndexChange to own the state. The canvas animates along the shortest path around the loop.
See the latest from ABUI.
Our annual letter explores the trends defining the component economy, including faster adoption of registries, design tokens everywhere, and agentic UI generation.
const [active, setActive] = React.useState(0)<SqueezeCarousel items={items} activeIndex={active} onActiveIndexChange={setActive}> ...</SqueezeCarousel>// Anywhere else on the page:<button onClick={() => setActive(3)}>Jump to item 4</button>imageHeight={50} makes the images exactly 50svh tall on desktop and mobile, and borderRadius={16} rounds the cards.
See the latest from ABUI.
Our annual letter explores the trends defining the component economy, including faster adoption of registries, design tokens everywhere, and agentic UI generation.
<SqueezeCarousel items={items} imageHeight={50} borderRadius={16}> ...</SqueezeCarousel>import { SqueezeCarousel, SqueezeCarouselHeader, SqueezeCarouselHeading, SqueezeCarouselTitle, SqueezeCarouselDescription, SqueezeCarouselControls, SqueezeCarouselPrevious, SqueezeCarouselNext, SqueezeCarouselCanvas, SqueezeCarouselDetails, SqueezeCarouselDetail, SqueezeCarouselDetailCopy, SqueezeCarouselDetailTitle, SqueezeCarouselDetailDescription, SqueezeCarouselDetailAction, SqueezeCarouselActionIcon, SqueezeCarouselMobileList, SqueezeCarouselMobileItem, SqueezeCarouselMobileMedia, type SqueezeCarouselItem,} from "@/components/squeeze-carousel"// Keep the array reference stable (module scope or useMemo).const items = [ { id: "annual-letter", title: "Businesses on ABUI shipped 1.9 million components in 2025.", description: "Our annual letter explores the trends defining the component economy.", cta: "Read the letter", href: "/annual-letter", src: "/news/annual-letter.jpg", // 16:9, e.g. 1632×918 mobileSrc: "/news/annual-letter-m.jpg", // 336:350, e.g. 672×700 }, // ... at least 5 items for a full strip] satisfies Array<SqueezeCarouselItem & { description: string; cta: string; href: string }>export function WhatsHappening() { // Rendered once for the desktop details and once for the mobile cards. const renderCopy = (item: (typeof items)[number]) => ( <> <SqueezeCarouselDetailCopy> <SqueezeCarouselDetailTitle>{item.title}</SqueezeCarouselDetailTitle> <SqueezeCarouselDetailDescription>{item.description}</SqueezeCarouselDetailDescription> </SqueezeCarouselDetailCopy> <SqueezeCarouselDetailAction asChild> <a href={item.href}> {item.cta} <SqueezeCarouselActionIcon /> </a> </SqueezeCarouselDetailAction> </> ) return ( <SqueezeCarousel items={items}> <SqueezeCarouselHeader> <SqueezeCarouselHeading> <SqueezeCarouselTitle>What's happening</SqueezeCarouselTitle> <SqueezeCarouselDescription>See the latest from ABUI.</SqueezeCarouselDescription> </SqueezeCarouselHeading> <SqueezeCarouselControls> <SqueezeCarouselPrevious /> <SqueezeCarouselNext /> </SqueezeCarouselControls> </SqueezeCarouselHeader> <SqueezeCarouselCanvas /> <SqueezeCarouselDetails> {(item, index) => ( <SqueezeCarouselDetail index={index}>{renderCopy(item)}</SqueezeCarouselDetail> )} </SqueezeCarouselDetails> <SqueezeCarouselMobileList> {(item, index) => ( <SqueezeCarouselMobileItem index={index}> <SqueezeCarouselMobileMedia /> {renderCopy(item)} </SqueezeCarouselMobileItem> )} </SqueezeCarouselMobileList> </SqueezeCarousel> )}href; modifier keys open a new tabimageHeight sizes the images as a percentage of the small viewport height, borderRadius rounds every cardcollapsedOffsetX keeps an image's focal point visible while collapseddata-slot, data-state and data-index attributes on every part for external styling<SqueezeCarousel items={items}> <SqueezeCarouselHeader> <SqueezeCarouselHeading> <SqueezeCarouselTitle /> <SqueezeCarouselDescription /> </SqueezeCarouselHeading> <SqueezeCarouselControls> <SqueezeCarouselPrevious /> <SqueezeCarouselNext /> </SqueezeCarouselControls> </SqueezeCarouselHeader> <SqueezeCarouselCanvas /> {/* >= sm */} <SqueezeCarouselDetails> {/* >= sm */} <SqueezeCarouselDetail index={i}> <SqueezeCarouselDetailCopy> <SqueezeCarouselDetailTitle /> <SqueezeCarouselDetailDescription /> </SqueezeCarouselDetailCopy> <SqueezeCarouselDetailAction> <SqueezeCarouselActionIcon /> </SqueezeCarouselDetailAction> </SqueezeCarouselDetail> </SqueezeCarouselDetails> <SqueezeCarouselMobileList> {/* < sm */} <SqueezeCarouselMobileItem index={i}> <SqueezeCarouselMobileMedia /> <SqueezeCarouselDetailCopy>…</SqueezeCarouselDetailCopy> <SqueezeCarouselDetailAction>…</SqueezeCarouselDetailAction> </SqueezeCarouselMobileItem> </SqueezeCarouselMobileList></SqueezeCarousel>Extends React.ComponentProps<"section">.
items - SqueezeCarouselItem[] (required) - Items to show. Keep the reference stable; a new array re-creates the canvas engineactiveIndex - number - Controlled active indexdefaultActiveIndex - number - Initial index when uncontrolled (default: 0)onActiveIndexChange - (index: number) => void - Fired on every change, in both modesborderRadius - number - Card corner radius in px (default: 6). Also exposed as --squeeze-carousel-radiusimageHeight - number - Image height as a percentage of the small viewport height (svh), applied to the canvas and the mobile cards. Omit to keep the default 1232:460 and 336:350 aspect ratiosonActiveItemClick - (item, index, event) => void - Called when the revealed card is clicked. Default: activate the item's SqueezeCarouselDetailAction, or open item.hrefcrossOrigin - "anonymous" | "use-credentials" - Applied to canvas images. Only needed if you read pixels back from the canvasannounce - (item, index, count) => string - Live-region text (default: "Item 2 of 8: title"). Use it for localisationid - string (required) - Stable keytitle - string (required) - Plain-text name, used for the live-region announcement and the default mobile alt textsrc - string (required) - Desktop canvas image, 16:9mobileSrc - string - Mobile card image, 336:350 (default: src)alt - string - Mobile image alt text (default: title). Pass "" for decorative imageshref - string - Link opened when the revealed card is clicked and no action is rendered for the itemcollapsedOffsetX - number - Horizontal image shift in px while the card is collapsed, positive moves the image left (default: 0)Extend the type with your own fields (description, cta, href) and read them in the render function.
SqueezeCarouselHeader, Heading, Controls, DetailCopy - plain containers extending their element propsSqueezeCarouselTitle (h2), Description (p), DetailTitle (h3), DetailDescription (p) - support asChild. Title is wired to the section's aria-labelledbySqueezeCarouselPrevious / Next - extend Button; default icon, aria-label and onClick can be overridden (call preventDefault to cancel navigation)SqueezeCarouselCanvas - extends canvas props; hidden below smSqueezeCarouselDetails / MobileList - children can be a render function (item, index) => ReactNodeSqueezeCarouselDetail / MobileItem - index: number (required)SqueezeCarouselDetailAction - extends Button (outline). Supports asChild for links. Manages tab order and focus on desktopSqueezeCarouselMobileMedia - renders the item image; pass children to replace ituseSqueezeCarousel() - context hook exposing activeIndex, goTo, goToNext, goToPrevious for custom controlsTab - Previous, Next, then the active item's action only← / → on Previous or Next - move one item, focus stays on the button← / → on the action - move one item and focus the new actionHome / End on the action - jump to the first or last itemsection with aria-roledescription="carousel" labelled by the titlearia-hidden); all content lives in the DOM copygroup with aria-roledescription="slide"; inactive details are aria-hidden with their action at tabindex=-1prefers-reduced-motion: short tweens, no hover squeeze, no crossfade, no mobile parallaxDesktop images are cover-fitted into a 16:9 box as tall as the canvas and centred on their card, so supply 16:9 images of at least 1632×918 with the subject near the centre. Use collapsedOffsetX when a subject sits off-centre.
Mobile images are shown at 336:350; 672×700 is a good size. Images load with the browser cache only, so serve already-optimised files or a CDN URL with size parameters.
The strip shows 4 main columns plus 1 to 3 slivers (5 to 7 cards). With fewer than 5 items the engine repeats items so the strip stays full. Five or more items is recommended.