Turn Into Dropdown
A fully accessible block transformation dropdown for Tiptap editors. Convert between different content types like headings, paragraphs, lists, blockquotes, and code blocks with an intuitive dropdown interface.
Installation
Add the component via the Tiptap CLI:
npx @tiptap/cli@latest add turn-into-dropdown
Components
<TurnIntoDropdown />
A comprehensive dropdown component for transforming block types in a Tiptap editor.
Usage
import { EditorContent, EditorContext, useEditor } from '@tiptap/react'
import { StarterKit } from '@tiptap/starter-kit'
import { TurnIntoDropdown } from '@/components/tiptap-ui/turn-into-dropdown'
import '@/components/tiptap-node/paragraph-node/paragraph-node.scss'
import '@/components/tiptap-node/heading-node/heading-node.scss'
import '@/components/tiptap-node/list-node/list-node.scss'
import '@/components/tiptap-node/blockquote-node/blockquote-node.scss'
import '@/components/tiptap-node/code-block-node/code-block-node.scss'
export default function MyEditor() {
const editor = useEditor({
immediatelyRender: false,
extensions: [StarterKit],
content: `
<h1>Document Title</h1>
<p>Welcome to the rich text editor. You can transform this paragraph into different block types.</p>
<ul>
<li>Transform any block element</li>
<li>Choose from multiple content types</li>
<li>Maintain content while changing structure</li>
</ul>
`,
})
return (
<EditorContext.Provider value={{ editor }}>
<div className="toolbar">
<TurnIntoDropdown
editor={editor}
hideWhenUnavailable={false}
blockTypes={['paragraph', 'heading', 'bulletList', 'orderedList', 'blockquote']}
portal={false}
useCardLayout={true}
onOpenChange={(isOpen) => console.log('Dropdown toggled:', isOpen)}
/>
</div>
<EditorContent editor={editor} role="presentation" />
</EditorContext.Provider>
)
}
Props
Name | Type | Default | Description |
---|---|---|---|
editor | Editor | null | undefined | The Tiptap editor instance |
hideWhenUnavailable | boolean | false | Hides dropdown when block transformation unavailable |
blockTypes | string[] | All supported types | Which block types to show in the dropdown |
portal | boolean | false | Whether to render dropdown menu in a portal |
useCardLayout | boolean | true | Whether to use card layout for dropdown content |
onOpenChange | (isOpen: boolean) => void | undefined | Callback fired when dropdown state changes |
<TurnIntoDropdownContent />
The dropdown content component that renders the available block type options.
Usage
import { TurnIntoDropdownContent } from '@/components/tiptap-ui/turn-into-dropdown'
function CustomDropdownContent() {
return (
<TurnIntoDropdownContent
blockTypes={['paragraph', 'heading', 'bulletList']}
useCardLayout={false}
/>
)
}
Props
Name | Type | Default | Description |
---|---|---|---|
blockTypes | string[] | All | Which block types to show |
useCardLayout | boolean | true | Whether to wrap content in a card layout |
Hooks
useTurnIntoDropdown()
A custom hook to build your own turn into dropdown with full control over rendering and behavior.
Usage
import { useTurnIntoDropdown } from '@/components/tiptap-ui/turn-into-dropdown'
function MyTurnIntoDropdown() {
const {
isVisible,
canToggle,
isOpen,
activeBlockType,
handleOpenChange,
filteredOptions,
label,
Icon,
} = useTurnIntoDropdown({
editor: myEditor,
hideWhenUnavailable: true,
blockTypes: ['paragraph', 'heading', 'bulletList'],
onOpenChange: (isOpen) => console.log('Dropdown toggled:', isOpen),
})
if (!isVisible) return null
return (
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<button disabled={!canToggle} aria-label={label}>
<span>{activeBlockType?.label || 'Text'}</span>
<Icon />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{filteredOptions.map((option) => (
<DropdownMenuItem key={option.type}>{option.label}</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
Props
Name | Type | Default | Description |
---|---|---|---|
editor | Editor | null | undefined | The Tiptap editor instance |
hideWhenUnavailable | boolean | false | Hides dropdown when block transformation unavailable |
blockTypes | string[] | All types | Which block types to show in the dropdown |
onOpenChange | (isOpen: boolean) => void | undefined | Callback fired when dropdown state changes |
Return Values
Name | Type | Description |
---|---|---|
isVisible | boolean | Whether the dropdown should be rendered |
canToggle | boolean | If block transformation is currently allowed |
isOpen | boolean | Whether the dropdown is currently open |
setIsOpen | (open: boolean) => void | Function to set dropdown open state |
activeBlockType | BlockTypeOption | Currently active block type information |
handleOpenChange | (open: boolean) => void | Function to handle dropdown open/close |
filteredOptions | BlockTypeOption[] | Filtered list of available block type options |
label | string | Accessible label text for the dropdown |
Icon | React.FC | Icon component for the dropdown trigger |
Block Types
The Turn Into Dropdown supports transformation between various block types:
Supported Block Types
- paragraph: Regular text paragraphs
- heading: Headings (levels 1, 2, and 3)
- bulletList: Bulleted (unordered) lists
- orderedList: Numbered (ordered) lists
- taskList: To-do lists with checkboxes
- blockquote: Quote blocks for highlighted text
- codeBlock: Code blocks with syntax highlighting
Block Type Options
Each block type includes:
interface BlockTypeOption {
type: string // The node type name
label: string // Display label
level?: number // For headings (1, 2, 3)
isActive: (editor: Editor) => boolean // Function to check if active
}
Utilities
canTurnInto(editor, allowedBlockTypes?)
Checks if block transformation can be performed in the current editor state.
import { canTurnInto } from '@/components/tiptap-ui/turn-into-dropdown'
const canTransform = canTurnInto(editor, ['paragraph', 'heading'])
if (canTransform) {
console.log('Block transformation is available')
}
Parameters
Name | Type | Description |
---|---|---|
editor | Editor | null | The Tiptap editor instance |
allowedBlockTypes | string[] | Optional array of allowed block types |
Returns
boolean
- Whether block transformation can be performed.
getFilteredBlockTypeOptions(blockTypes?)
Gets filtered block type options based on available types.
import { getFilteredBlockTypeOptions } from '@/components/tiptap-ui/turn-into-dropdown'
const options = getFilteredBlockTypeOptions(['paragraph', 'heading', 'bulletList'])
console.log(
'Available options:',
options.map((o) => o.label),
)
Parameters
Name | Type | Description |
---|---|---|
blockTypes | string[] | Optional array of block types to filter |
Returns
BlockTypeOption[]
- Filtered array of block type options.
getActiveBlockType(editor, blockTypes?)
Gets the currently active block type from the available options.
import { getActiveBlockType } from '@/components/tiptap-ui/turn-into-dropdown'
const activeType = getActiveBlockType(editor, ['paragraph', 'heading'])
console.log('Current block type:', activeType?.label)
Parameters
Name | Type | Description |
---|---|---|
editor | Editor | null | The Tiptap editor instance |
blockTypes | string[] | Optional array of allowed block types |
Returns
BlockTypeOption
- Currently active block type option.
shouldShowTurnInto(params)
Determines if the turn into dropdown should be visible based on editor state.
import { shouldShowTurnInto } from '@/components/tiptap-ui/turn-into-dropdown'
const shouldShow = shouldShowTurnInto({
editor: myEditor,
hideWhenUnavailable: true,
blockTypes: ['paragraph', 'heading'],
})
Parameters
Name | Type | Description |
---|---|---|
params.editor | Editor | null | The Tiptap editor instance |
params.hideWhenUnavailable | boolean | Whether to hide when transformation unavailable |
params.blockTypes | string[] | Optional array of allowed block types |
Returns
boolean
- Whether the dropdown should be shown.
Behavior and Constraints
Selection Requirements
The Turn Into Dropdown works with different selection types:
- Text Selection: Works within block elements
- Node Selection: Works when entire blocks are selected
- Empty Selection: Works when cursor is placed within a block
Supported Transformations
The component intelligently handles transformations between different block types:
- Content Preservation: Text content is maintained during transformation
- Structure Changes: Block structure changes while preserving inline formatting
- List Handling: Supports conversion between different list types
- Heading Levels: Allows selection of different heading levels
Editor State Dependencies
- Editable Editor: Only works when the editor is in editable mode
- Valid Block Context: Requires cursor to be within a transformable block
- Extension Availability: Requires relevant extensions (StarterKit covers most cases)
Integration Examples
With Custom Block Types
function EditorWithCustomBlocks() {
const customBlockTypes = ['paragraph', 'heading', 'bulletList', 'blockquote']
return <TurnIntoDropdown blockTypes={customBlockTypes} hideWhenUnavailable={true} />
}
With Portal Rendering
function FloatingTurnIntoDropdown() {
return (
<TurnIntoDropdown
portal={true} // Renders in a portal for better z-index control
useCardLayout={false} // Simplified layout
hideWhenUnavailable={true}
/>
)
}
With State Tracking
function EditorWithStateTracking() {
const [currentBlockType, setCurrentBlockType] = useState<string>('paragraph')
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
// Update tracking when dropdown closes
const activeType = getActiveBlockType(editor)
setCurrentBlockType(activeType?.type || 'paragraph')
}
}
return (
<div>
<p>Current block type: {currentBlockType}</p>
<TurnIntoDropdown onOpenChange={handleOpenChange} />
</div>
)
}
Custom Dropdown Implementation
function CustomTurnIntoDropdown() {
const { isVisible, canToggle, activeBlockType, filteredOptions, handleOpenChange } =
useTurnIntoDropdown({
hideWhenUnavailable: true,
blockTypes: ['paragraph', 'heading', 'bulletList', 'orderedList'],
})
if (!isVisible) return null
return (
<div className="custom-turn-into">
<select
disabled={!canToggle}
value={activeBlockType?.type || 'paragraph'}
onChange={(e) => {
const option = filteredOptions.find((opt) => opt.type === e.target.value)
if (option?.onClick) {
option.onClick()
}
}}
>
{filteredOptions.map((option) => (
<option key={option.type} value={option.type}>
{option.label}
</option>
))}
</select>
</div>
)
}
Requirements
Dependencies
@tiptap/react
- Core Tiptap React integration
Extensions
Any extensions that provide the block types you want to support:
@tiptap/starter-kit
- Provides most common block types- Individual extensions for specific block types
Referenced Components
use-tiptap-editor
(hook)text-button
,heading-button
,list-button
,blockquote-button
,code-block-button
(ui components)button
,button-group
(primitives)dropdown-menu
(primitive)card
(primitive)chevron-down-icon
(icon)