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 '';
}
}typescript
Animation MagicThe 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 EffectI'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!