Viewport Menu in Puck Composition Patterns
Description
Currently, Puck provides excellent composition patterns that allow developers to create custom layouts and override components like header, iframe, and outline. However, when using these composition patterns, the native viewport menu (the dropdown that allows switching between different device sizes/viewports) becomes inaccessible or completely disappears from the UI.
This creates a significant limitation for developers who want to customize Puck's layout while maintaining the viewport switching functionality. The viewport menu is a core feature for responsive design editing, and losing access to it when using composition patterns forces developers to choose between customization and functionality.
The expected outcome is to either:
- Have access to the viewport menu component so it can be placed in custom compositions
- Have pre-built composition components that include viewport functionality
This would allow developers to maintain full viewport switching capabilities while still being able to customize Puck's layout and appearance.
Considerations
- This affects all developers using Puck's composition patterns who need viewport switching
- The viewport menu component likely has internal state and logic that needs to be preserved
- The solution should maintain the existing viewport configuration and behavior
- The API should be consistent with other Puck composition patterns
- TypeScript support should be maintained for proper typing
- The solution should work with custom viewport configurations passed to the main Puck component
- Backward compatibility should be maintained for existing compositions
Proposals
Proposal 1: Expose Viewport Menu Component
Expose the internal viewport menu component so it can be used in custom compositions:
import { Puck } from '@measured/puck';
// Custom composition with viewport menu
<Puck>
<div className="custom-layout">
<div className="custom-header">
<Puck.ViewportMenu />
{/* Other custom header content */}
</div>
<div className="custom-preview">
<Puck.Preview />
</div>
</div>
</Puck>
Pros:
- Maximum flexibility for developers
- Maintains existing composition patterns
- Allows precise placement of viewport menu
- Consistent with current API design
Cons:
- Requires exposing internal components
- Developers need to handle layout themselves
- More complex implementation for simple use cases
Proposal 2: ViewportPreview Composition Component
Create a new high-level composition component that includes both preview and viewport menu:
import { Puck} from '@measured/puck';
<Puck>
<div className="custom-layout">
<div className="custom-header">
{/* Custom header content */}
</div>
{/* This includes both the Menu and the Preview together */}
<Puck.ViewportPreview className="custom-preview" />
</div>
</Puck>
Pros:
- Simple to use for common cases
- Maintains viewport functionality out of the box
- Less prone to implementation errors
- Good default behavior
Cons:
- Less flexible than exposing individual components
- May not fit all layout requirements
- Fixed positioning of viewport menu relative to preview
Proposal 3: ViewportFrame with Sub-components
Create a ViewportFrame component with sub-components for more granular control:
import { Puck } from '@measured/puck';
<Puck>
<div className="custom-layout">
<Puck.ViewportFrame>
<Puck.ViewportFrame.Menu className="custom-viewport-menu" />
<Puck.ViewportFrame.Preview className="custom-preview" />
</Puck.ViewportFrame>
</div>
</Puck>
Pros:
- Good balance between flexibility and ease of use
- Clear component hierarchy
- Maintains viewport state management
- Allows custom styling while preserving functionality
Cons:
- Introduces new component hierarchy
- More complex than a single component solution
Hey @muchisx,
This is something that comes up quite a lot, so thank you for opening a feature request to track it.
The current limitation in terms of implementation is that the viewport resizing logic can’t reliably support custom interfaces, because Puck doesn’t know what your editor layout constraints are:
https://github.com/puckeditor/puck/blob/main/packages/core/components/Puck/components/Canvas/index.tsx#L171-L237
The workaround right now is to:
- Constrain the size of
<Puck.Preview>to match the target device dimensions. - If the preview exceeds the bounds of your custom UI layout, use
transform: scale()to scale it down.
That transform: scale() is also what enables zooming. That said, I agree we should provide a less convoluted way of implementing this logic.
In terms of the options you mentioned, I think Proposal 1 seems the most flexible and consistent with the rest of the composable components API, but I'd like to hear what others have to say about this. Custom UIs are meant for developers to define their own layouts, so that "con" doesn't seem too problematic to me. Normally, if you don’t want to override the layout, you should go with overrides instead.
Speaking of which, this could be paired with an override for viewport controls, which is mentioned and tracked here: #1150.
@FedericoBonel Do you have an estimate for when we might expect this?
Hey @mits87! not at the moment, but we have it in our radar