🚧 Slots Customization
Markput uses the slots pattern (popularized by Material-UI) to give you fine-grained control over internal components. This guide covers how to customize the container and text rendering without losing built-in functionality.
What are Slots?
Section titled “What are Slots?”Slots are a component customization pattern that separates structure from styling and behavior. Instead of wrapping or forking components, you customize them through props:
<MarkedInput slots={{ container: CustomContainer, // Replace component paragraph: CustomParagraph, // Replace component }} slotProps={{ container: {className: 'my-editor'}, // Pass props to the container row: {className: 'my-row'}, // Pass props to every row }}/>Key Concepts:
slots- Replace the default component entirelyslotProps- Pass props to the default (or custom) component
Available Slots
Section titled “Available Slots”| Slot | Default Component | Purpose |
|---|---|---|
container |
<div> |
Root editable container — the one contenteditable |
paragraph |
<div> |
The component a row with NO kind renders through |
slotProps key |
Reaches |
|---|---|
container |
The container element |
row |
Every row’s wrapper |
slots and slotProps do not take the same keys, and the names say why: slots.paragraph is
consulted only for a row with NO kind, while slotProps.row is merged onto EVERY row’s wrapper —
kind and paragraph alike.
What’s NOT a slot:
- Mark components (use the
Markprop, oroption.Mark) - Overlay components (use the
Overlayprop, oroption.Overlay) - Plain text segments (use the
Spanprop) - A row kind’s component (use
option.row.Component— see Row Kinds)
Both key sets are declared on the published types, in both adapters: Slots
and SlotProps each extend the core contract, so a key core resolves
is a key TypeScript accepts. A slot value may be a component or an intrinsic tag name —
slots={{container: 'article'}} mounts the editor on an <article>.
Using slotProps (Customize Defaults)
Section titled “Using slotProps (Customize Defaults)”The simplest way to customize slots is through slotProps. This passes props to the default components without replacing them.
Basic Styling
Section titled “Basic Styling”import {MarkedInput} from '@markput/react'
function StyledEditor() { const [value, setValue] = useState('')
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: { style: { border: '2px solid #e0e0e0', borderRadius: '8px', padding: '12px', minHeight: '120px', fontSize: '16px', lineHeight: '1.6', }, }, row: { style: { whiteSpace: 'pre-wrap', // Preserve whitespace }, }, }} /> )}CSS Classes
Section titled “CSS Classes”<MarkedInput Mark={MyMark} slotProps={{ container: { className: 'editor-container', }, row: { className: 'editor-row', }, }}/>.editor-container { border: 1px solid #ddd; border-radius: 4px; padding: 16px; font-family: 'Inter', sans-serif;}
.editor-container:focus { outline: 2px solid #2196f3; border-color: transparent;}
.editor-row { color: #333; letter-spacing: 0.01em;}Event Handlers
Section titled “Event Handlers”Add event handlers through slotProps:
function EditorWithEvents() { const [value, setValue] = useState('') const [isFocused, setIsFocused] = useState(false)
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slotProps={{ container: { onFocus: (e: React.FocusEvent<HTMLDivElement>) => { console.log('Editor focused') setIsFocused(true) }, onBlur: (e: React.FocusEvent<HTMLDivElement>) => { console.log('Editor blurred') setIsFocused(false) }, onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => { if (e.key === 's' && (e.metaKey || e.ctrlKey)) { e.preventDefault() console.log('Save triggered') } }, onPaste: (e: React.ClipboardEvent<HTMLDivElement>) => { console.log('Pasted:', e.clipboardData.getData('text')) }, style: { outline: isFocused ? '2px solid blue' : 'none', }, }, }} /> )}Accessibility Attributes
Section titled “Accessibility Attributes”Improve accessibility with ARIA attributes:
<MarkedInput Mark={MyMark} slotProps={{ container: { role: 'textbox', 'aria-label': 'Message input', 'aria-multiline': true, 'aria-required': true, 'aria-describedby': 'editor-help-text' } }}/>
<p id="editor-help-text" className="help-text"> Type @ to mention someone</p>Data Attributes
Section titled “Data Attributes”Add custom data attributes for testing or analytics:
<MarkedInput Mark={MyMark} slotProps={{ container: { 'data-testid': 'editor-input', 'data-editor-type': 'mention-editor', 'data-track': 'user-input', }, row: { 'data-doc-row': true, }, }}/>Using slots (Replace Components)
Section titled “Using slots (Replace Components)”For deeper customization, replace the default components entirely with slots.
Custom Container Component
Section titled “Custom Container Component”Replace the container with a custom component:
import {forwardRef} from 'react'import type {HTMLAttributes} from 'react'
const CustomContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>((props, ref) => { return ( <div {...props} ref={ref} style={{ ...props.style, border: '2px dashed #9c27b0', borderRadius: '12px', padding: '16px', backgroundColor: '#f5f5f5', }} /> )})
function Editor() { return ( <MarkedInput Mark={MyMark} slots={{ container: CustomContainer, }} /> )}Important: Custom slot components MUST:
- Accept all props with spread (
{...props}) - Forward the ref (
forwardRef) - Be typed correctly for TypeScript
Custom Paragraph Component
Section titled “Custom Paragraph Component”slots.paragraph is the component a row with NO kind renders through — the default is a bare div.
Spread everything you are given onto the element you render, the row’s rendered content included:
const Paragraph = ({children, ref, className, style}: RowProps) => ( <p ref={ref} className={className} style={style}> {children} </p>)
const Editor = () => ( <MarkedInput Mark={MyMark} slots={{paragraph: Paragraph}} />)A row that MATCHED a kind never reaches this slot; it renders through option.row.Component. See
Row Kinds.
Custom Text Component
Section titled “Custom Text Component”Plain text is not a slot — it is the Span prop, and it is a special case: the element it renders IS
the surface core writes the token’s text into, so it cannot be wrapped and it must not be given a
second writer.
// `SpanProps` is the text token's props AND the ref core writes text throughconst Mono = ({value, ref}: SpanProps) => ( <span ref={ref} style={{fontFamily: 'monospace', letterSpacing: '0.5px'}}> {value} </span>)
const Editor = () => ( <MarkedInput Mark={MyMark} Span={Mono} />)The ref must land on the element that shows the text. A component that drops it leaves the text
unbound and the caret cannot resolve into it.
The prop carries that contract, so an inline Span needs no annotation at all — value and ref
are typed from where it is written:
const Editor = () => <MarkedInput Mark={MyMark} Span={({value, ref}) => <span ref={ref}>{value}</span>} />Combining slots and slotProps
Section titled “Combining slots and slotProps”Use both together - slots to replace components, slotProps to pass additional props:
const CustomContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( (props, ref) => ( <div {...props} ref={ref} className={`custom-editor ${props.className || ''}`} /> ))
const Editor = () => ( <MarkedInput Mark={MyMark} slots={{ container: CustomContainer // Custom component }} slotProps={{ container: { className: 'with-shadow', // Props passed to CustomContainer onFocus: () => console.log('Focused') } }} />)The props from slotProps.container will be passed to your CustomContainer component.
Styling Approaches
Section titled “Styling Approaches”Approach 1: Inline Styles
Section titled “Approach 1: Inline Styles”Good for dynamic styles based on state:
function ThemedEditor() { const [theme, setTheme] = useState('light')
const containerStyle = { backgroundColor: theme === 'light' ? '#fff' : '#1e1e1e', color: theme === 'light' ? '#000' : '#fff', border: `1px solid ${theme === 'light' ? '#ddd' : '#444'}`, }
return ( <MarkedInput Mark={MyMark} slotProps={{ container: {style: containerStyle}, }} /> )}Approach 2: CSS Classes
Section titled “Approach 2: CSS Classes”Good for static styles and media queries:
<MarkedInput Mark={MyMark} slotProps={{ container: {className: 'editor-modern'}, }}/>.editor-modern { border: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 16px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);}
.editor-modern:focus { box-shadow: 0 10px 60px rgba(0, 0, 0, 0.3);}
@media (max-width: 768px) { .editor-modern { padding: 12px; border-radius: 8px; }}Approach 3: CSS-in-JS
Section titled “Approach 3: CSS-in-JS”Good for component libraries and scoped styles:
import {styled} from '@mui/material/styles'
const StyledContainer = styled('div')(({theme}: any) => ({ border: `1px solid ${theme.palette.divider}`, borderRadius: theme.shape.borderRadius, padding: theme.spacing(2), backgroundColor: theme.palette.background.paper, '&:focus': { outline: `2px solid ${theme.palette.primary.main}`, outlineOffset: 2, },}))
const StyledParagraph = styled('div')(({theme}: any) => ({ color: theme.palette.text.primary, fontSize: theme.typography.body1.fontSize,}))
function MuiEditor() { return ( <MarkedInput Mark={MyMark} slots={{ container: StyledContainer, paragraph: StyledParagraph, }} /> )}Approach 4: Tailwind CSS
Section titled “Approach 4: Tailwind CSS”Good for utility-first styling:
const TailwindContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( (props, ref) => ( <div {...props} ref={ref} className={` border border-gray-300 rounded-lg p-4 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 dark:border-gray-600 min-h-[120px] ${props.className || ''} `} /> ))
const Editor = () => ( <MarkedInput Mark={MyMark} slots={{ container: TailwindContainer }} />)Common Use Cases
Section titled “Common Use Cases”Use Case 1: Placeholder Text
Section titled “Use Case 1: Placeholder Text”Show placeholder when editor is empty:
const ContainerWithPlaceholder = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement> & {isEmpty?: boolean}>( ({isEmpty, ...props}, ref) => ( <div {...props} ref={ref} style={{position: 'relative'}}> {isEmpty && ( <div style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none', color: '#999', padding: 'inherit', }} > Type @ to mention someone... </div> )} </div> ))
function EditorWithPlaceholder() { const [value, setValue] = useState('')
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slots={{ container: ContainerWithPlaceholder, }} slotProps={{ container: { isEmpty: value.trim() === '', }, }} /> )}Use Case 2: Character Counter
Section titled “Use Case 2: Character Counter”Add a character count overlay:
const ContainerWithCounter = forwardRef< HTMLDivElement, HTMLAttributes<HTMLDivElement> & {charCount?: number; maxChars?: number}>(({charCount = 0, maxChars = 500, ...props}, ref) => ( <div style={{position: 'relative'}}> <div {...props} ref={ref} /> <div style={{ position: 'absolute', bottom: 8, right: 8, fontSize: '12px', color: charCount > maxChars ? '#f44336' : '#999', pointerEvents: 'none', }} > {charCount} / {maxChars} </div> </div>))
function EditorWithCounter() { const [value, setValue] = useState('')
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slots={{ container: ContainerWithCounter, }} slotProps={{ container: { charCount: value.length, maxChars: 500, }, }} /> )}Use Case 3: Custom Focus Behavior
Section titled “Use Case 3: Custom Focus Behavior”Highlight container on focus:
const FocusableContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( (props, ref) => { const [focused, setFocused] = useState(false)
return ( <div {...props} ref={ref} onFocus={(e) => { setFocused(true) props.onFocus?.(e) }} onBlur={(e) => { setFocused(false) props.onBlur?.(e) }} style={{ ...props.style, border: focused ? '2px solid #2196f3' : '1px solid #ddd', boxShadow: focused ? '0 0 0 3px rgba(33, 150, 243, 0.1)' : 'none', transition: 'all 0.2s ease' }} /> ) })
const Editor = () => ( <MarkedInput Mark={MyMark} slots={{ container: FocusableContainer }} />)Use Case 4: Line Numbers
Section titled “Use Case 4: Line Numbers”Add line numbers for multi-line content:
const ContainerWithLineNumbers = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement> & {lineCount?: number}>( ({lineCount = 1, ...props}, ref) => ( <div style={{display: 'flex'}}> <div style={{ width: '40px', backgroundColor: '#f5f5f5', padding: '8px', textAlign: 'right', color: '#999', fontSize: '12px', userSelect: 'none', borderRight: '1px solid #ddd', }} > {Array.from({length: lineCount}, (_, i) => ( <div key={i}>{i + 1}</div> ))} </div> <div {...props} ref={ref} style={{flex: 1, ...props.style}} /> </div> ))
function EditorWithLineNumbers() { const [value, setValue] = useState('') const lineCount = value.split('\n').length
return ( <MarkedInput value={value} onChange={setValue} Mark={MyMark} slots={{ container: ContainerWithLineNumbers, }} slotProps={{ container: { lineCount, }, }} /> )}Use Case 5: Syntax Highlighting for Text
Section titled “Use Case 5: Syntax Highlighting for Text”Custom text rendering with highlighting, through the Span prop:
import type {SpanProps} from '@markput/react'
const Highlighted = ({value = '', ref}: SpanProps) => { const isUrl = /^https?:\/\//.test(value) const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
const style = isUrl ? {color: '#2196f3', textDecoration: 'underline'} : isEmail ? {color: '#4caf50'} : undefined
return ( <span ref={ref} style={style}> {value} </span> )}
const Editor = () => ( <MarkedInput Mark={MyMark} Span={Highlighted} />)The styling reacts to the token’s text, but the text on screen is core’s to write: keep the ref on
the element and put nothing beside the value inside it.
Integration with UI Libraries
Section titled “Integration with UI Libraries”Material-UI (MUI)
Section titled “Material-UI (MUI)”import {Paper, useTheme} from '@mui/material'import {styled} from '@mui/material/styles'
const MuiContainer = styled(Paper)(({theme}: any) => ({ padding: theme.spacing(2), minHeight: 120, border: `1px solid ${theme.palette.divider}`, '&:focus-within': { borderColor: theme.palette.primary.main, boxShadow: `0 0 0 2px ${theme.palette.primary.main}25`, },}))
function MuiEditor() { return ( <MarkedInput Mark={MyMark} slots={{ container: MuiContainer, }} slotProps={{ container: { elevation: 0, }, }} /> )}Chakra UI
Section titled “Chakra UI”import { Box } from '@chakra-ui/react'import { forwardRef } from 'react'
const ChakraContainer = forwardRef((props, ref) => ( <Box {...props} ref={ref} borderWidth="1px" borderRadius="md" p={4} minH="120px" _focus={{ borderColor: 'blue.500', boxShadow: 'outline' }} />))
const Editor = () => ( <MarkedInput Mark={MyMark} slots={{ container: ChakraContainer }} />)Ant Design
Section titled “Ant Design”import { Input } from 'antd'import { forwardRef } from 'react'
const AntContainer = forwardRef<HTMLDivElement, any>((props, ref) => ( <div {...props} ref={ref} className="ant-input" style={{ minHeight: 120, ...props.style }} />))
const Editor = () => ( <MarkedInput Mark={MyMark} slots={{ container: AntContainer }} />)TypeScript Usage
Section titled “TypeScript Usage”Typing Custom Slot Components
Section titled “Typing Custom Slot Components”import {forwardRef} from 'react'import type {HTMLAttributes, CSSProperties} from 'react'
// Type container with custom propsinterface CustomContainerProps extends HTMLAttributes<HTMLDivElement> { variant?: 'outlined' | 'filled' error?: boolean}
const TypedContainer = forwardRef<HTMLDivElement, CustomContainerProps>( ({variant = 'outlined', error = false, ...props}, ref) => { const style: CSSProperties = { ...props.style, border: error ? '2px solid red' : '1px solid #ddd', backgroundColor: variant === 'filled' ? '#f5f5f5' : 'transparent', }
return <div {...props} ref={ref} style={style} /> })
// Usage with type safetyfunction TypedEditor() { return ( <MarkedInput Mark={MyMark} slots={{ container: TypedContainer, }} slotProps={{ container: { variant: 'filled', error: true, }, }} /> )}Generic Slot Props
Section titled “Generic Slot Props”import type {MarkedInputProps} from '@markput/react'
interface EditorProps { containerClass?: string}
function ConfigurableEditor({containerClass}: EditorProps) { const slotProps: MarkedInputProps['slotProps'] = { container: { className: containerClass, }, }
return <MarkedInput Mark={MyMark} slotProps={slotProps} />}Performance Considerations
Section titled “Performance Considerations”Memoize Custom Components
Section titled “Memoize Custom Components”Prevent unnecessary re-renders:
import { memo, forwardRef } from 'react'
const MemoizedContainer = memo( forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( (props, ref) => ( <div {...props} ref={ref} className="editor-container" /> ) ))
const Editor = () => ( <MarkedInput Mark={MyMark} slots={{ container: MemoizedContainer }} />)Avoid Inline Function Creation
Section titled “Avoid Inline Function Creation”// ❌ Bad - creates new function each render;<MarkedInput slotProps={{ container: { onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => console.log(e.key), }, }}/>
// ✅ Good - stable function referencefunction Editor() { const handleKeyDown = useCallback((e: KeyboardEvent) => { console.log(e.key) }, [])
return ( <MarkedInput slotProps={{ container: { onKeyDown: handleKeyDown, }, }} /> )}Memoize slotProps Object
Section titled “Memoize slotProps Object”function Editor() { const slotProps = useMemo( () => ({ container: { className: 'editor', style: {padding: '16px'}, }, }), [] ) // Only created once
return <MarkedInput Mark={MyMark} slotProps={slotProps} />}Complete Examples
Section titled “Complete Examples”Example 1: GitHub-Style Editor
Section titled “Example 1: GitHub-Style Editor”import {MarkedInput} from '@markput/react'import {useState, forwardRef} from 'react'import type {HTMLAttributes} from 'react'
const GitHubContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>((props, ref) => ( <div {...props} ref={ref} style={{ ...props.style, border: '1px solid #d0d7de', borderRadius: '6px', padding: '8px 12px', fontSize: '14px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif', minHeight: '100px', backgroundColor: '#fff', }} />))
function GitHubEditor() { const [value, setValue] = useState('')
return ( <div style={{maxWidth: '800px'}}> <div style={{ border: '1px solid #d0d7de', borderRadius: '6px', overflow: 'hidden', }} > <div style={{ backgroundColor: '#f6f8fa', padding: '8px 12px', borderBottom: '1px solid #d0d7de', fontSize: '14px', color: '#57606a', }} > Write a comment </div> <MarkedInput value={value} onChange={setValue} Mark={({value}) => <span style={{color: '#0969da', fontWeight: 600}}>@{value}</span>} slots={{ container: GitHubContainer, }} slotProps={{ container: { 'aria-label': 'Comment body', }, }} options={[ { markup: '@[__value__]', overlay: {trigger: '@', data: ['octocat', 'github', 'copilot']}, }, ]} /> </div> </div> )}Example 2: Notion-Style Editor
Section titled “Example 2: Notion-Style Editor”const NotionContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>((props, ref) => { const [placeholder, setPlaceholder] = useState("Type '/' for commands")
return ( <div {...props} ref={ref} style={{ ...props.style, fontSize: '16px', lineHeight: '1.6', fontFamily: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif', padding: '12px 96px', minHeight: '300px', outline: 'none', }} data-placeholder={placeholder} onFocus={() => setPlaceholder('')} onBlur={e => { if (!e.currentTarget.textContent) setPlaceholder("Type '/' for commands") props.onBlur?.(e) }} /> )})
function NotionEditor() { const [value, setValue] = useState('')
return ( <MarkedInput value={value} onChange={setValue} Mark={({value}) => ( <span style={{ backgroundColor: '#f1f1ef', padding: '2px 6px', borderRadius: '3px', fontSize: '85%', fontFamily: 'monospace', }} > {value} </span> )} slots={{container: NotionContainer}} options={[ {overlay: {trigger: '/'}}, {markup: '# __slot__', row: {Component: Heading}, menu: {label: 'Heading 1'}}, {markup: '- __slot__', row: {Component: Bullet, continues: true, indents: true}, menu: {label: 'Bulleted list'}}, ]} draggable /> )}The container must render whatever it is given: the editor’s own children ARE the document. Paint a
placeholder with CSS on [data-placeholder]:empty::before rather than by replacing them. What makes
this a Notion-shaped editor is the row vocabulary beside it — see Row Kinds.
Best Practices
Section titled “Best Practices”type ContainerProps = HTMLAttributes<HTMLDivElement>
// Always forward refsconst ForwardingContainer = forwardRef<HTMLDivElement, ContainerProps>((props, ref) => <div {...props} ref={ref} />)
// Spread all propsconst SpreadingContainer = forwardRef<HTMLDivElement, ContainerProps>((props, ref) => ( <div {...props} ref={ref} className={`custom ${props.className || ''}`} />))
// Preserve existing styleconst StyleKeepingContainer = forwardRef<HTMLDivElement, ContainerProps>((props, ref) => ( <div {...props} ref={ref} style={{...props.style, padding: '16px'}} />))
// Memoize stable componentsconst StableContainer = memo( forwardRef<HTMLDivElement, ContainerProps>((props, ref) => <div {...props} ref={ref} />))❌ Don’t
Section titled “❌ Don’t”// Don't forget forwardRefconst Bad = (props) => <div {...props} /> // Missing ref!
// Don't forget to spread propsconst Bad = forwardRef((props, ref) => ( <div ref={ref} className="custom" /> // Lost all props!))
// Don't override style completelyconst Bad = forwardRef((props, ref) => ( <div {...props} ref={ref} style={{ padding: '16px' }} /> // Lost original style!))
// Don't use inline componentsconst Editor = () => ( <MarkedInput slots={{ container: (props) => <div {...props} /> // Creates new component each render! }} />
// Don't forget TypeScript types const Bad = forwardRef((props, ref) => ( // Any types! <div {...props} ref={ref} /> )))