Back to home

writing a dynamic island component

Posted on saturday, 2nd Nov, 2024

one of my personal favorite from my components, even worked on building a dynamic island inspired component at rocketium for all the ai flows and events. while building it, i have explored many aspects of how design and animations are contolled via states and conditional actions in a robust way. also how extensive the stylings need to be for future applications. has been really fun and i would love to share the 'recipe' with you in this one! the shared example and approach are very different from what we built at rocketium.

๐Ÿงฎ a real-life playable demo of the component

vercelshadcn

๐Ÿงช Understanding the Concept

The Dynamic Island is a shape-shifting UI element that adapts its size and appearance based on different states. Think of it as a living piece of your interface that smoothly morphs to show different types of content.

My implementation has three main states:

  • IDLE (130px width): The default pill-shaped state. Small, subtle, and unobtrusive - perfect for sitting quietly at the top of your screen.
  • SUGGESTIVE (220px width): A wider state that provides a gentle visual cue that something is happening. Great for short notifications or status updates.
  • EXPANDED (260px width): The fully expanded state where the component scales up to 120% with a spring animation. It takes on a more rectangular shape while keeping those slick rounded corners.

What makes this component special is how it transitions between states. I've added some neat features:

  • Smooth spring animations that give it a natural, iOS-like feel.
  • A subtle blur effect during transitions.
  • Dynamic border radius changes that match the original Apple implementation.
  • A float effect using shadows to make it feel like it's sitting above the interface.

Step 1: Setting Up the Foundation ๐Ÿงฑ

First, I'll define the types and interfaces. This gives us a solid foundation and makes the component type-safe:

export enum DYNAMIC_ISLAND_STATE {
  IDLE = 'IDLE',
  SUGGESTIVE = 'SUGGESTIVE',
  EXPANDED = 'EXPANDED',
}

export interface DynamicIslandProps extends React.HTMLAttributes<HTMLDivElement> {}

typescript

Step 2: Creating the Context ๐ŸŒด

To manage the state globally, I'll need a context. This allows any child component to access and modify the Dynamic Island's state:

export type DynamicIslandContextType = {
  state: DYNAMIC_ISLAND_STATE;
  setState: React.Dispatch<SetStateAction<DYNAMIC_ISLAND_STATE>>;
};

const INITIAL_DYNAMIC_ISLAND_CONTEXT_DATA = {
  state: DYNAMIC_ISLAND_STATE.IDLE,
} as const;

typescript

Step 3: Implementing the Provider ๐Ÿ•น๏ธ

The provider component wraps the application and manages the state:

export function DynamicIslandProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<DYNAMIC_ISLAND_STATE>(
    INITIAL_DYNAMIC_ISLAND_CONTEXT_DATA.state
  );

  return (
    <DynamicIslandContext.Provider value={{ state, setState }}>
      {children}
    </DynamicIslandContext.Provider>
  );
}

typescript

Step 4: Building the Core Component ๐ŸŽจ

Now for the exciting part - the actual Dynamic Island component! Let me break down the key features:

State-Based Styling

I use helper functions to determine the width and border radius based on the current state:

function getDynamicIslandWidthByState(state: DYNAMIC_ISLAND_STATE): string {
  switch (state) {
    case DYNAMIC_ISLAND_STATE.IDLE: return '130px';
    case DYNAMIC_ISLAND_STATE.SUGGESTIVE: return '220px';
    case DYNAMIC_ISLAND_STATE.EXPANDED: return '260px';
    default: return '';
  }
}

function getDynamicIslandBorderRaduisByState(state: DYNAMIC_ISLAND_STATE): string {
  switch (state) {
    case DYNAMIC_ISLAND_STATE.IDLE: return 'rounded-full';
    case DYNAMIC_ISLAND_STATE.SUGGESTIVE: return 'rounded-full';
    case DYNAMIC_ISLAND_STATE.EXPANDED: return 'rounded-3xl';
    default: return '';
  }
}

typescript

Animation Magic

The component uses Framer Motion for smooth transitions:

<motion.div
  className={cn(
    'dynamic-island p-2 bg-black overflow-hidden font-sans text-white shadow-md shadow-black/20',
    getDynamicIslandBorderRaduisByState(state),
    className,
  )}
  animate={{
    width: getDynamicIslandWidthByState(state),
    scale: state === DYNAMIC_ISLAND_STATE.EXPANDED ? 1.2 : 1,
    filter: showBlur ? 'blur(2px)' : 'blur(0px)',
  }}
  transition={{
    type: 'spring',
    stiffness: 100,
    bounce: 0,
    filter: {
      type: 'spring',
      duration: 0.2,
    },
  }}
>

typescript

Blur Effect

I've added a subtle blur effect during state transitions to make them feel more polished:

useEffect(() => {
  setShowBlur(true);
  const showBlurTimeout = setTimeout(() => setShowBlur(false), 200);
  return () => clearTimeout(showBlurTimeout);
}, [state]);

typescript

The above implementation will give you something like this,

Best Practices and Tips ๐ŸŽ€

  • Dynamic Sizing: Each state has its own predefined width, creating a smooth transition between states.
  • Border Radius Transitions: The component adapts its shape using Tailwind's border radius classes - from fully rounded in IDLE and SUGGESTIVE states to slightly rounded in EXPANDED state.
  • Animation Strategy,
    • Spring animations provide a natural, iOS-like feel
    • The blur effect adds polish during state transitions
    • Scale transformation in the expanded state creates a subtle pop effect
  • Styling Strategy: Tailwind classes are composed using the cn utility function, making it easy to combine dynamic and static classes.

โœจ final notes

this is a very low level implementation of this component. as the scale increases, the shared my not work at it's absolute best, but can be scaled easily. be creative and happy building!