Hover-Expand Icon Strip
moumenlab/hover-expand-icon-stripFreeComponent
Icons in a fixed-width box that expand on hover or Tab while the others squeeze to fit.
Live preview
live · rendered by the registry
Rendered by moumenlab on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://lab.moumen.dev/r/hover-expand-icon-strip.jsonSource
1"use client";23// Hover-expand icon strip - a design-engineered fixed-width row.4//5// N icons (3–4) share one fixed-width box. Hovering a panel grows it while the6// others squeeze to make room, so the row's outer width never changes. The7// motion is a pure `flex-grow` tween (1 → 5) inside a `flex-basis: 0` row.8// Keyboard parity: focus-visible expands a panel exactly like hover (motion's9// whileFocus), so tabbing the strip feels the same as mousing it.10//11// The label reveal is deliberately asymmetric: it enters late (a ~90ms delay12// gives the panel room to open before the text lands, so no crushed clipped word13// mid-expansion) with opacity + rise + blur, and exits fast with no delay so it14// ducks out before the shrinking panel squeezes it - each variant carries its15// own transition. Inspect (blueprint) mode swaps the name for live math.16//17// Animation: motion/react (variants propagate hover/focus from the panel to its18// icon and label). Requires the lab-theme tokens. Fully Tailwind, no CSS files.1920import type { ReactNode } from "react";21import { motion, MotionConfig, type Variants } from "motion/react";2223const EASE = [0.22, 1, 0.36, 1] as const;2425export interface IconStripItem {26 label: string;27 icon: ReactNode;28}2930const iconVariants: Variants = {31 rest: { y: 0, transition: { duration: 0.35, ease: EASE } },32 expanded: { y: "-0.5rem", transition: { duration: 0.35, ease: EASE } },33};3435const labelVariants: Variants = {36 rest: {37 opacity: 0,38 y: "0.25rem",39 filter: "blur(3px)",40 transition: { duration: 0.15, ease: "easeIn" },41 },42 expanded: {43 opacity: 1,44 y: 0,45 filter: "blur(0px)",46 transition: { duration: 0.35, ease: EASE, delay: 0.09 },47 },48};4950export default function ExpandingIconStrip({51 items,52 width = 320,53 inspect = false,54 onSelect,55}: {56 items: IconStripItem[];57 width?: number;58 inspect?: boolean;59 onSelect?: (item: IconStripItem) => void;60}) {61 return (62 <MotionConfig reducedMotion="user">63 <div64 className={65 "relative flex gap-2 h-16" +66 (inspect67 ? " outline outline-[1.5px] outline-dashed outline-[#3b82f6] outline-offset-[0.375rem] rounded-[0.25rem]"68 : "")69 }70 data-inspect={inspect ? "true" : "false"}71 style={{ width: `${width}px`, maxWidth: "100%" }}72 >73 {items.map((item) => (74 <motion.button75 key={item.label}76 type="button"77 initial="rest"78// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from moumenlab
All 14 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Caret-Anchored Mention Popovercaret-mention-popover | moumenlab | Components | Free | 1 dep | |
| Command Palette with Argument Chipscommand-palette | moumenlab | Components | Free | 1 dep | |
| Drag-to-Reorder Listdrag-to-reorder-list | moumenlab | Components | Free | 1 dep | |
| File Upload Staging Areafile-upload-staging | moumenlab | Components | Free | 1 dep | |
| Inertial Wheel Listinertial-wheel-list | moumenlab | Components | Free | 1 dep | |
| Morphing Checkout Flowmorphing-checkout | moumenlab | Components | Free | 1 dep | |
| OTP Segmented Inputotp-segmented-input | moumenlab | Components | Free | 1 dep | |
| Schedule Builderschedule-builder | moumenlab | Components | Free | 1 dep |
