Dynamic Island
CraftFloating glass pill that resizes to fit what it shows (status, selection, progress). Powers the data-table bulk bar.
Motion: Shape springs to the new size; old content blur-fades out under the new.
Craft
Installation
npx shadcn@latest add @craft/dynamic-islandSource
components/ui/dynamic-island.tsx
"use client"
import * as React from "react"
import { AnimatePresence, motion } from "motion/react"
import { cn } from "cn"
import { morphContent, spring } from "@/lib/motion"
/**
* A floating glass pill that morphs its size to fit whatever it shows — the
* Webcraftsman header / iOS Dynamic Island. Change `view` whenever the content
* changes meaning (idle → "3 selected" → uploading…) and the shape springs to
* the new size while the old content blur-fades out under the new.
* It only shapes; position it yourself (e.g. `fixed bottom-6 left-1/2 -translate-x-1/2`).
* Put live status in `aria-live` content if it should be announced.
*/
function DynamicIsland({
view,
className,
children,
...props
}: Omit<React.ComponentProps<typeof motion.div>, "children" | "layout"> & {
/** Identifies what's shown; changing it morphs to the new content */
view: React.Key
children: React.ReactNode
}) {
return (
<motion.div
data-slot="dynamic-island"
layout
transition={spring.morph}
// 28px keeps a pill up to 56px tall and reads as rounded-3xl when it grows taller
style={{ borderRadius: 28 }}
className={cn("relative isolate overflow-hidden border glass text-sm text-popover-foreground", className)}
{...props}
>
<AnimatePresence mode="popLayout" initial={false}>
<motion.div key={view} layout="position" variants={morphContent} initial="hidden" animate="visible" exit="exit">
{children}
</motion.div>
</AnimatePresence>
</motion.div>
)
}
export { DynamicIsland }