Squeeze Carousel
Open in
Demo

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.

What's happening

See the latest from ABUI.

  • Businesses on ABUI shipped 1.9 million components in 2025.

    Our annual letter explores the trends defining the component economy, including faster adoption of registries, design tokens everywhere, and agentic UI generation.

    Read the letter
  • 150K+ teams had their best deploy day ever.

    From Black Friday through Cyber Monday, registries served more than 40 billion component installs while maintaining 99.9999% uptime.

    See the numbers
  • The vertical SaaS design-system benchmark report.

    Learn what is driving growth for product teams in 2025: going multi-brand, embedding motion, and integrating AI into the core of their workflows.

    Get the data
  • Two founders sit down to talk about the future of interfaces.

    Hear them discuss the choices that shaped their products, the future of composable UI, and their advice for people starting out today.

    Watch video
  • New tools to ship interfaces outside app stores.

    New platforms mean new opportunities. Read how web components and registries help you ship the same UI everywhere, with more control over your release cycle.

    Learn how
  • A new partnership to enable better open-source components.

    Learn how the partnership helps you tap into a global catalogue of audited, accessible components directly from your editor.

    View announcement
  • Make your components discoverable by AI agents.

    Find out how the registry protocol allows any agent to install, compose and document your components without requiring any major technical changes.

    Read more
  • How leading teams unify product experiences and drive growth.

    Get insights into how other brands unified their web and native experiences and optimised their onboarding for a seamless first run.

    Get the report
Item 1 of 8: Businesses on ABUI shipped 1.9 million components in 2025.
Controlled

Pass activeIndex and onActiveIndexChange to own the state. The canvas animates along the shortest path around the loop.

What's happening

See the latest from ABUI.

  • Businesses on ABUI shipped 1.9 million components in 2025.

    Our annual letter explores the trends defining the component economy, including faster adoption of registries, design tokens everywhere, and agentic UI generation.

    Read the letter
  • 150K+ teams had their best deploy day ever.

    From Black Friday through Cyber Monday, registries served more than 40 billion component installs while maintaining 99.9999% uptime.

    See the numbers
  • The vertical SaaS design-system benchmark report.

    Learn what is driving growth for product teams in 2025: going multi-brand, embedding motion, and integrating AI into the core of their workflows.

    Get the data
  • Two founders sit down to talk about the future of interfaces.

    Hear them discuss the choices that shaped their products, the future of composable UI, and their advice for people starting out today.

    Watch video
  • New tools to ship interfaces outside app stores.

    New platforms mean new opportunities. Read how web components and registries help you ship the same UI everywhere, with more control over your release cycle.

    Learn how
  • A new partnership to enable better open-source components.

    Learn how the partnership helps you tap into a global catalogue of audited, accessible components directly from your editor.

    View announcement
  • Make your components discoverable by AI agents.

    Find out how the registry protocol allows any agent to install, compose and document your components without requiring any major technical changes.

    Read more
  • How leading teams unify product experiences and drive growth.

    Get insights into how other brands unified their web and native experiences and optimised their onboarding for a seamless first run.

    Get the report
Item 1 of 8: Businesses on ABUI shipped 1.9 million components in 2025.
Reactcontrolled.tsx
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>
Viewport height & radius

imageHeight={50} makes the images exactly 50svh tall on desktop and mobile, and borderRadius={16} rounds the cards.

What's happening

See the latest from ABUI.

  • Businesses on ABUI shipped 1.9 million components in 2025.

    Our annual letter explores the trends defining the component economy, including faster adoption of registries, design tokens everywhere, and agentic UI generation.

    Read the letter
  • 150K+ teams had their best deploy day ever.

    From Black Friday through Cyber Monday, registries served more than 40 billion component installs while maintaining 99.9999% uptime.

    See the numbers
  • The vertical SaaS design-system benchmark report.

    Learn what is driving growth for product teams in 2025: going multi-brand, embedding motion, and integrating AI into the core of their workflows.

    Get the data
  • Two founders sit down to talk about the future of interfaces.

    Hear them discuss the choices that shaped their products, the future of composable UI, and their advice for people starting out today.

    Watch video
  • New tools to ship interfaces outside app stores.

    New platforms mean new opportunities. Read how web components and registries help you ship the same UI everywhere, with more control over your release cycle.

    Learn how
  • A new partnership to enable better open-source components.

    Learn how the partnership helps you tap into a global catalogue of audited, accessible components directly from your editor.

    View announcement
  • Make your components discoverable by AI agents.

    Find out how the registry protocol allows any agent to install, compose and document your components without requiring any major technical changes.

    Read more
  • How leading teams unify product experiences and drive growth.

    Get insights into how other brands unified their web and native experiences and optimised their onboarding for a seamless first run.

    Get the report
Item 1 of 8: Businesses on ABUI shipped 1.9 million components in 2025.
Reactviewport-height.tsx
<SqueezeCarousel items={items} imageHeight={50} borderRadius={16}>  ...</SqueezeCarousel>

Usage

Reactwhats-happening.tsx
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>  )}

Features

  • Canvas-rendered image strip: one 16:9 primary card plus progressively narrower companions, each cover-fitted and clipped to a rounded rectangle, so cards squeeze instead of scaling
  • Hovering a column stretches it and squeezes its neighbours; hovering a sliver widens it by 3px
  • Clicking any non-primary column advances by that many steps; the strip loops infinitely
  • Clicking the revealed card activates the item's action (router links included) or opens href; modifier keys open a new tab
  • imageHeight sizes the images as a percentage of the small viewport height, borderRadius rounds every card
  • 1s ease-out-expo tween of every width, gap and image offset; 200ms and no hover squeeze under reduced motion
  • Per-item collapsedOffsetX keeps an image's focal point visible while collapsed
  • Copy crossfades below the canvas; inactive copy is hidden from assistive tech and the tab order
  • Scroll-snap mobile layout with scale, parallax and fade driven by scroll progress, all in CSS
  • Controlled or uncontrolled active index via the Radix controllable-state hook
  • Compound component API: every part is its own element, spreads native props and exports its Props type
  • data-slot, data-state and data-index attributes on every part for external styling
  • Lazy initialisation: the canvas engine and images only load when the block is near the viewport
  • Device-pixel-ratio aware backing store (capped at 2x), ResizeObserver driven, fully disposed on unmount
  • Theme-aware: only uses design tokens, works in light and dark mode
  • No runtime dependencies beyond the registry Button, the Radix Slot and controllable-state hooks and lucide icons

Anatomy

Reactanatomy.tsx
<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>

Props

SqueezeCarousel

Extends React.ComponentProps<"section">.

  • items - SqueezeCarouselItem[] (required) - Items to show. Keep the reference stable; a new array re-creates the canvas engine
  • activeIndex - number - Controlled active index
  • defaultActiveIndex - number - Initial index when uncontrolled (default: 0)
  • onActiveIndexChange - (index: number) => void - Fired on every change, in both modes
  • borderRadius - number - Card corner radius in px (default: 6). Also exposed as --squeeze-carousel-radius
  • imageHeight - 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 ratios
  • onActiveItemClick - (item, index, event) => void - Called when the revealed card is clicked. Default: activate the item's SqueezeCarouselDetailAction, or open item.href
  • crossOrigin - "anonymous" | "use-credentials" - Applied to canvas images. Only needed if you read pixels back from the canvas
  • announce - (item, index, count) => string - Live-region text (default: "Item 2 of 8: title"). Use it for localisation

SqueezeCarouselItem

  • id - string (required) - Stable key
  • title - string (required) - Plain-text name, used for the live-region announcement and the default mobile alt text
  • src - string (required) - Desktop canvas image, 16:9
  • mobileSrc - string - Mobile card image, 336:350 (default: src)
  • alt - string - Mobile image alt text (default: title). Pass "" for decorative images
  • href - string - Link opened when the revealed card is clicked and no action is rendered for the item
  • collapsedOffsetX - 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.

Sub-components

  • SqueezeCarouselHeader, Heading, Controls, DetailCopy - plain containers extending their element props
  • SqueezeCarouselTitle (h2), Description (p), DetailTitle (h3), DetailDescription (p) - support asChild. Title is wired to the section's aria-labelledby
  • SqueezeCarouselPrevious / Next - extend Button; default icon, aria-label and onClick can be overridden (call preventDefault to cancel navigation)
  • SqueezeCarouselCanvas - extends canvas props; hidden below sm
  • SqueezeCarouselDetails / MobileList - children can be a render function (item, index) => ReactNode
  • SqueezeCarouselDetail / MobileItem - index: number (required)
  • SqueezeCarouselDetailAction - extends Button (outline). Supports asChild for links. Manages tab order and focus on desktop
  • SqueezeCarouselMobileMedia - renders the item image; pass children to replace it
  • useSqueezeCarousel() - context hook exposing activeIndex, goTo, goToNext, goToPrevious for custom controls

Keyboard

  • Tab - 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 action
  • Home / End on the action - jump to the first or last item
  • Focusing an inactive action (for example via find-in-page) makes its item active

Accessibility

  • Root is a section with aria-roledescription="carousel" labelled by the title
  • The canvas is decorative (aria-hidden); all content lives in the DOM copy
  • Each detail is a group with aria-roledescription="slide"; inactive details are aria-hidden with their action at tabindex=-1
  • A polite live region announces "Item N of M: title" on every change
  • Controls are real buttons with accessible names inside a labelled group
  • Honors prefers-reduced-motion: short tweens, no hover squeeze, no crossfade, no mobile parallax

Image guidelines

Desktop 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.