Editor Suite — 19 components for building rich text editing experiences.
Inspired by TipTap — the most popular headless rich text editor (2.5M+ weekly npm downloads, built on ProseMirror, used by Notion, Linear, Vercel, and more) — this module provides all the building blocks needed to assemble a full-featured editor.
Research: Most Popular Rich Text Editors (2025)
| Editor | Weekly DLs | Foundation | Used by |
|---|---|---|---|
| TipTap | 2.5M+ | ProseMirror | Notion clones, Linear, Vercel |
| Quill v2 | 1.8M+ | Custom | Slack, LinkedIn, Figma, Airtable |
| Lexical | 1.2M+ | Custom (Meta) | Facebook, WhatsApp Web |
| ProseMirror | 900K+ | — | Base for TipTap/Remirror |
| CKEditor 5 | 600K+ | Custom | Enterprise / CMS |
TipTap wins on DX: headless, framework-agnostic, tree-shakable, collaborative.
advanced_editor/1 is a PhiaUI transpilation of TipTap's editor design.
Components
Group A — Toolbar Primitives (no JS hooks)
editor_toolbar/1—role="toolbar"wrappertoolbar_button/1— single action button (active, disabled, size variants)toolbar_group/1—role="group"semantic groupingtoolbar_separator/1— verticalrole="separator"divider
Group B — Floating / Contextual (JS hooks)
bubble_menu/1— floating toolbar above text selection (PhiaBubbleMenu)floating_menu/1— block-insertion menu at empty cursor (PhiaFloatingMenu)slash_command_menu/1—/trigger command palette (PhiaSlashCommand)
Group C — Enhanced Inline Editing
inline_edit/1— extended editable with type/validation/buttons (PhiaEditable)inline_edit_group/1— wrapper for bulk-editing multiple inline fields
Group D — Rich Text Utilities
editor_color_picker/1— text/bg color palette toolbar dropdown (PhiaEditorColorPicker)editor_toolbar_dropdown/1— generic toolbar dropdown (PhiaEditorDropdown)editor_link_dialog/1— modal for insert/edit linkeditor_code_block/1— language badge + copy button + line numberseditor_character_count/1— char/word counter with optional progress barmarkdown_editor/1— split-pane textarea + preview (PhiaMarkdownEditor)rich_text_viewer/1— read-only HTML container with prose sizingeditor_find_replace/1— slide-in search+replace bar (PhiaEditorFindReplace)editor_word_count/1— words / reading-time display widget
Bonus — TipTap-Inspired Full Editor
advanced_editor/1— complete WYSIWYG combining all primitives (PhiaAdvancedEditor)
Summary
Functions
Full-featured WYSIWYG editor inspired by TipTap's design.
Renders a floating toolbar that appears above the user's text selection.
Renders a server-side character/word counter with optional progress bar.
Renders a styled code block with a header bar (language badge, filename, copy button).
Renders a color palette picker toolbar dropdown for text/background colors.
Renders a slide-in search-and-replace bar for a contenteditable editor.
Renders a modal dialog for inserting or editing a hyperlink.
Renders a role="toolbar" container for editor controls.
Renders a generic dropdown inside a toolbar (heading level, font size, etc.).
Renders a standalone words / characters / reading-time display widget.
Renders a block-insertion menu that floats at the empty cursor line.
Enhanced inline editable field. Extends editable/1 with input type variants,
validation errors, loading state, and optional confirm/cancel buttons.
Wrapper for grouping multiple inline_edit/1 fields that share a bulk save/cancel flow.
Renders a split-pane Markdown editor: textarea (write) + server-rendered preview.
Renders a read-only Phoenix.HTML.raw container with prose sizing.
Renders a / trigger command palette for block-level actions.
Renders a single toolbar action button.
Renders a role="group" semantic group for related toolbar buttons.
Renders a thin vertical role="separator" divider between toolbar groups.
Functions
Full-featured WYSIWYG editor inspired by TipTap's design.
TipTap is the most popular headless rich text editor (built on ProseMirror,
2.5M+ weekly npm downloads). This component transpiles TipTap's UX patterns
to PhiaUI: same toolbar layout, bubble menus, slash commands, and inline SVG icons —
powered entirely by vanilla JS + execCommand via the PhiaAdvancedEditor hook.
Features:
- Full toolbar: undo/redo, heading dropdown, marks (bold/italic/underline/strike/code), lists (bullet/ordered), links (insert/remove), text color picker, find & replace toggle
- Bubble menu for quick inline formatting of selected text
- Floating menu at empty cursor for block insertion
- Find & replace bar (Ctrl/Cmd+F)
- Live word/character count footer
Phoenix.HTML.FormFieldintegration via hidden<input>- Dark mode support via
.darkclass
Example
<%!-- Standalone --%>
<.advanced_editor id="blog-editor" placeholder="Write your post..." min_height="400px" />
<%!-- Form-integrated --%>
<.form for={@form} phx-submit="publish">
<.advanced_editor id="post-editor" field={@form[:body]} show_word_count />
</.form>Hook registration
import PhiaAdvancedEditor from "./hooks/advanced_editor"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { PhiaAdvancedEditor, PhiaBubbleMenu, PhiaEditorColorPicker, PhiaEditorFindReplace }
})Attributes
id(:string) (required)field(Phoenix.HTML.FormField) - Defaults tonil.value(:string) - Defaults tonil.placeholder(:string) - Defaults to"Type '/' for commands, or start writing...".min_height(:string) - Defaults to"300px".toolbar_variant(:atom) - Defaults to:default. Must be one of:default,:compact, or:floating.show_word_count(:boolean) - Defaults totrue.show_find_replace(:boolean) - Defaults tofalse.class(:string) - Defaults tonil.
Renders a server-side character/word counter with optional progress bar.
Drives via phx-change on the editor: the LiveView counts characters/words
and assigns :count. The progress bar fills and changes colour as count nears max.
Example
<.editor_character_count count={@char_count} max={500} mode={:characters} show_bar />
<.editor_character_count count={@word_count} max={100} mode={:words} />
<.editor_character_count count={@char_count} mode={:both} />Attributes
count(:integer) - Defaults to0.max(:integer) - Defaults tonil.mode(:atom) - Defaults to:characters. Must be one of:characters,:words, or:both.show_bar(:boolean) - Defaults tofalse.class(:string) - Defaults tonil.
Renders a styled code block with a header bar (language badge, filename, copy button).
The copy button reuses PhiaCopyButton via data-value={copy_value}.
Pass copy_value with the raw code string for clipboard support.
Example
<.editor_code_block id="my-snippet" language="elixir" filename="hello.ex"
copy_value={@raw_code}>
def hello, do: IO.puts("Hello, world!")
</.editor_code_block>Attributes
id(:string) - Defaults to"editor-code-block".language(:string) - Defaults tonil.filename(:string) - Defaults tonil.show_copy(:boolean) - Defaults totrue.copy_value(:string) - Defaults tonil.show_line_numbers(:boolean) - Defaults tofalse.class(:string) - Defaults tonil.
Slots
inner_block(required)
Renders a color palette picker toolbar dropdown for text/background colors.
The PhiaEditorColorPicker hook dispatches execCommand(action, false, hex)
on swatch click and tracks the current color via queryCommandValue on selection change.
Example
<.editor_color_picker action="foreColor" label="Text color" />
<.editor_color_picker action="hiliteColor" label="Highlight" value="#EAB308" />Attributes
id(:string) - Defaults tonil.action(:string) - Defaults to"foreColor".label(:string) - Defaults to"Text color".colors(:list) - Defaults to[].value(:string) - Defaults tonil.class(:string) - Defaults tonil.
Renders a slide-in search-and-replace bar for a contenteditable editor.
The PhiaEditorFindReplace hook opens on Ctrl/Cmd+F inside the editor,
injects <mark> elements for matches, and provides find prev/next and replace.
Example
<.editor_find_replace id="find-bar" editor_id="my-editor" />Attributes
id(:string) (required)editor_id(:string) (required)class(:string) - Defaults tonil.
Renders a modal dialog for inserting or editing a hyperlink.
Fields: URL, title (optional), open-in-new-tab checkbox.
The dialog is shown/hidden programmatically via the editor hook calling
document.getElementById(id).classList.remove('hidden').
Example
<.editor_link_dialog id="link-dialog" on_submit="insert_link" />Attributes
id(:string) (required)on_submit(:string) - Defaults tonil.initial_url(:string) - Defaults tonil.initial_title(:string) - Defaults tonil.class(:string) - Defaults tonil.
Renders a role="toolbar" container for editor controls.
Variants:
:default— bordered bar, wraps to multiple lines:floating— elevated popover-style (shadow, backdrop blur):compact— minimal single-line strip
Example
<.editor_toolbar aria_label="Formatting toolbar">
<.toolbar_button action="bold" aria_label="Bold">
<svg .../>
</.toolbar_button>
</.editor_toolbar>Attributes
id(:string) - Defaults tonil.variant(:atom) - Defaults to:default. Must be one of:default,:floating, or:compact.aria_label(:string) - Defaults to"Editor toolbar".class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Renders a generic dropdown inside a toolbar (heading level, font size, etc.).
The PhiaEditorDropdown hook toggles the panel on trigger click and dispatches
the selected item's data-action to the linked editor.
Example
<.editor_toolbar_dropdown id="heading-dd" label="Paragraph">
<:item value="paragraph" action="paragraph" label="Paragraph" />
<:item value="h1" action="h1" label="Heading 1" />
<:item value="h2" action="h2" label="Heading 2" />
<:item value="h3" action="h3" label="Heading 3" />
</.editor_toolbar_dropdown>Attributes
id(:string) (required)label(:string) (required)value(:string) - Defaults tonil.class(:string) - Defaults tonil.
Slots
item- Accepts attributes:value(:string) (required)action(:string)label(:string) (required)
Renders a standalone words / characters / reading-time display widget.
All computation is server-side — pass the raw text content.
Example
<.editor_word_count content={@body_text} show_reading_time words_per_minute={250} />Attributes
content(:string) - Defaults to"".show_reading_time(:boolean) - Defaults totrue.words_per_minute(:integer) - Defaults to200.class(:string) - Defaults tonil.
Enhanced inline editable field. Extends editable/1 with input type variants,
validation errors, loading state, and optional confirm/cancel buttons.
Uses the PhiaEditable hook (same as editable/1).
Example
<.inline_edit id="title-edit" value={@title} type={:text} on_submit="update_title"
show_buttons placeholder="Enter a title...">
<:preview>{@title}</:preview>
</.inline_edit>Attributes
id(:string) (required)value(:string) - Defaults tonil.type(:atom) - Defaults to:text. Must be one of:text,:number,:textarea, or:select.placeholder(:string) - Defaults to"Click to edit".on_submit(:string) - Defaults tonil.on_cancel(:string) - Defaults tonil.show_buttons(:boolean) - Defaults tofalse.loading(:boolean) - Defaults tofalse.error(:string) - Defaults tonil.required(:boolean) - Defaults tofalse.min(:any) - Defaults tonil.max(:any) - Defaults tonil.options(:list) - Defaults to[].class(:string) - Defaults tonil.
Slots
preview
Wrapper for grouping multiple inline_edit/1 fields that share a bulk save/cancel flow.
Pass custom action buttons via the :actions slot, or use on_save_all/on_cancel_all
to auto-render default Save all / Cancel buttons.
Example
<.inline_edit_group id="profile-edit" on_save_all="save_profile" on_cancel_all="reset_profile">
<.inline_edit id="name-edit" value={@name} on_submit="update_name" />
<.inline_edit id="email-edit" value={@email} on_submit="update_email" />
</.inline_edit_group>Attributes
id(:string) (required)on_save_all(:string) - Defaults tonil.on_cancel_all(:string) - Defaults tonil.class(:string) - Defaults tonil.
Slots
inner_block(required)actions
Renders a split-pane Markdown editor: textarea (write) + server-rendered preview.
The PhiaMarkdownEditor hook debounces textarea input events (300 ms) and
calls pushEvent(on_change, {raw, length, words}). The LiveView re-renders
with the updated preview_html after server-side Markdown parsing.
Example
<.markdown_editor id="body-editor" value={@draft} preview_html={@preview}
on_change="md_changed" label="Article body" />Attributes
id(:string) (required)value(:string) - Defaults tonil.preview_html(:string) - Defaults tonil.on_change(:string) - Defaults tonil.label(:string) - Defaults tonil.placeholder(:string) - Defaults to"Write in Markdown...".min_height(:string) - Defaults to"200px".class(:string) - Defaults tonil.
Slots
toolbar
Renders a read-only Phoenix.HTML.raw container with prose sizing.
Security: Always sanitize content server-side before passing to this
component. Use HtmlSanitizeEx or equivalent.
Example
<.rich_text_viewer content={@post.body} prose_size={:lg} />Attributes
content(:string) (required)prose_size(:atom) - Defaults to:base. Must be one of:sm,:base, or:lg.class(:string) - Defaults tonil.- Global attributes are accepted.
Renders a single toolbar action button.
The hook reads data-action to dispatch formatting commands and toggles
aria-pressed and the is-active CSS class automatically.
Example
<.toolbar_button action="bold" aria_label="Bold" tooltip="Bold (Ctrl+B)" active={@bold_active}>
<svg class="h-4 w-4" viewBox="0 0 16 16" fill="none">
<path d="M5 3h4.5a2.5 2.5 0 010 5H5V3zM5 8h5a2.5 2.5 0 010 5H5V8z"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</.toolbar_button>Attributes
action(:string) (required)aria_label(:string) (required)active(:boolean) - Defaults tofalse.disabled(:boolean) - Defaults tofalse.tooltip(:string) - Defaults tonil.size(:atom) - Defaults to:sm. Must be one of:xs,:sm, or:md.class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Renders a role="group" semantic group for related toolbar buttons.
Example
<.toolbar_group label="Text formatting">
<.toolbar_button action="bold" aria_label="Bold">...</.toolbar_button>
<.toolbar_button action="italic" aria_label="Italic">...</.toolbar_button>
</.toolbar_group>Attributes
label(:string) - Defaults tonil.class(:string) - Defaults tonil.
Slots
inner_block(required)
Renders a thin vertical role="separator" divider between toolbar groups.
Example
<.toolbar_group label="Marks">...</.toolbar_group>
<.toolbar_separator />
<.toolbar_group label="Blocks">...</.toolbar_group>Attributes
class(:string) - Defaults tonil.