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
๐งช 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 StylingI 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 ''; } }Animation Magictypescript
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, }, }} >Blur Effecttypescript
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!