Dynamic Island

Craft

Floating 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-island

Source

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 }