Novident Editor ships with a named paragraph style system modelled after word processors like Microsoft Word. Every paragraph can reference a reusable, named style that defines its visual appearance — font family, font size, bold, italic, spacing, alignment, colours, and more.
Styles are resolved through an inheritance chain (basedOn) and can be
applied, changed, or cleared at runtime through the toolbar or
programmatically.
┌──────────────────────────────────────────────────┐
│ NovidentStylesConfig │
│ ├─ registry: NovidentStyleRegistry │
│ │ └─ Map<String, NovidentStyleDefinition> │
│ ├─ defaultStyle: NovidentStyleDefinition │
│ └─ defaultStylesByType: Map<String, ...> │
└──────────────────────────────────────────────────┘
│
│ provided via InheritedWidget
▼
┌──────────────────────────────────────────────────┐
│ NovidentEditorStyles (InheritedWidget) │
│ └─ resolveStyle(Node) → NovidentStyleDefinition │
│ 1. node.attributes['styleRef'] → registry │
│ 2. defaultStylesByType[node.type] │
│ 3. config.defaultStyle │
└──────────────────────────────────────────────────┘
│
│ registry.resolve(id)
▼
┌──────────────────────────────────────────────────┐
│ NovidentStyleRegistry │
│ └─ resolve(id) → walks basedOn chain, merges │
│ parent.merge(child) │
└──────────────────────────────────────────────────┘
import 'package:novident_editor/novident_editor.dart';
// 1 — Define styles
const myStyles = NovidentStyleRegistry({
'base': NovidentStyleDefinition(
id: 'base',
name: 'Base',
fontSize: 12,
fontFamily: 'Arial',
indent: NovidentStyleIndent(firstLineIndent: 30),
),
'body': NovidentStyleDefinition.nextSame(
id: 'body',
name: 'Body',
basedOn: 'base', // inherits fontSize 12, fontFamily Arial
spacing: NovidentStyleSpacing(after: 8),
),
'heading-1': NovidentStyleDefinition(
id: 'heading-1',
name: 'Heading 1',
basedOn: 'base', // inherits fontFamily Arial
fontSize: 32, // overrides fontSize
bold: true,
spacing: NovidentStyleSpacing(before: 24, after: 12),
next: 'body', // Enter → next paragraph uses 'body'
// Ignores the first line indent specified
allowGlobalFirstLineIndent: false,
),
});
// 2 — Wire them into the editor
final editor = NovidentEditor(
editorState: editorState,
styles: NovidentStylesConfig(
registry: myStyles,
// font family and font size
// must be defined for default style
//
// if not, this will throw an assertio error
defaultStyle: myStyles.styles['base']!,
defaultStylesByType: {
'paragraph': myStyles.styles['body']!,
'heading': myStyles.styles['heading-1']!,
'table': {/* my table style */},
},
),
);Now every toolbar that shows styles (styleToolbarItem) will list "Base",
"Body", and "Heading 1". Selecting one applies its id as the block's
styleRef attribute, and the renderer resolves the full definition through
the basedOn chain.
When a block has styleRef: 'heading-1', the renderer calls:
final style = registry.resolve('heading-1');resolve walks the basedOn chain backwards:
heading-1 → basedOn: 'base'
base → no basedOn (root)
Then merges parent-first so child properties override parent:
base.fontSize = 12 ─┐
base.fontFamily = 'Arial' ├─ merge
heading-1.fontSize = 32 ─┘ → fontSize: 32, fontFamily: 'Arial'
Cyclic basedOn references are detected and silently broken (the chain
stops at the cycle point).
NovidentEditorStyles.resolveStyle(node) resolves the effective style in
three steps:
| Priority | Source | Example |
|---|---|---|
| 1 | node.attributes['styleRef'] → registry.resolve(id) |
Block explicitly styled as "heading-1" |
| 2 | config.defaultStylesByType[node.type] |
All heading blocks default to heading-1 style |
| 3 | config.defaultStyle |
Catch-all fallback (e.g., "body") |
Styles define the default appearance of a paragraph. Inline formatting
(applied via formatDelta — bold, italic, font family, font size, text
colour, etc.) overrides the style on a per-character basis:
Style "body" → fontSize: 12, fontFamily: 'Arial', color: black
└─ Inline → fontSize: 14 (only selected text)
└─ Inline → color: red (only selected text)
Resolution order (used by font/size/colour toolbar items):
| Priority | Source | Example |
|---|---|---|
| 1 | Delta inline attribute | attr['font_size'] = 14 |
| 2 | Resolved node style (basedOn merged) |
style.fontSize = 12 |
| 3 | Hard default | 12.0 / null |
When the cursor is collapsed (no selection range), the toolbar reads the
inline attribute from the character just before the cursor — the same
strategy used by toggleAttribute.
When there is no selection at all, the toolbar preserves the last known value via an internal cache.
Three desktop toolbar items interact with the style system:
final styleToolbarItem // already defined in novident_editorDropdown that lists all registered styles. Selecting one sets
blockComponentStyleRef on the current node. Clearing it removes the
explicit style (the block falls back to its type default or the global
default).
final fontItem = buildFontFamilyItem(
fontFamilies: ['Arial', 'Times New Roman', 'Courier New', 'Georgia'],
);Dropdown that applies NovidentRichTextKeys.fontFamily as an inline
attribute on the selected text. The current font is resolved from inline
attributes first, then from the resolved node style. Each font name is
rendered in its own typeface inside the dropdown.
final sizeItem = buildFontSizeItem(
minSize: 1,
maxSize: 99,
defaultSize: 12,
);Scrollable dropdown (like Word) that applies NovidentRichTextKeys.fontSize
as an inline attribute. Auto-scrolls to centre the current size when opened.
The current size is resolved from inline attributes first, then from the
resolved node style, falling back to defaultSize.
All three items support toolbar theming — they derive their colours from the
highlightColor and iconColor parameters passed by the toolbar's style
configuration, and optionally wrap themselves in a tooltip via
tooltipBuilder.
Using the NovidentStaticToolbar and with the correct configuration you can look results like this:
The package ships with kDefaultStyleRegistry and kDefaultBaseStyle:
// lib/src/core/style/default_styles.dart
const kDefaultBaseStyle = NovidentStyleDefinition.nextSame(
id: '__novident_base__',
name: 'Base',
fontSize: 12.0,
fontFamily: 'Roboto',
spacing: NovidentStyleSpacing(after: 5.0),
);
final kNormalBodyStyle = NovidentStyleDefinition.nextSame(
id: 'normal',
name: 'Normal',
basedOn: kDefaultBaseStyle.id, // inherits fontSize 12
);
// Also exports kDefaultHeadingStyles (heading-1 through heading-6)You can use these as-is, extend them, or build your own from scratch.
The nextSame constructor is a convenience that sets next to the style's
own id (so pressing Enter keeps the same style).
kDefaultBaseStyle always defines fontFamily: 'Roboto'. Every style that
inherits from it (directly or transitively via basedOn) is guaranteed
to resolve to a non-null font family — the editor never delegates font
selection to the platform.
If you provide your own defaultStyle, make sure it has fontFamily set.
An assertion at editor mount time validates this in debug mode.
NovidentFontProvider supplies the list of available font families and a
guaranteed non-null default. It is injected through NovidentEditor and
stored on EditorState.fontProvider:
NovidentEditor(
editorState: editorState,
fontProvider: NovidentFontProvider.fromList(
['Arial', 'Times New Roman', 'Courier New', 'Georgia'],
defaultFamily: 'Arial',
),
styles: ...,
);| Factory | Description |
|---|---|
NovidentFontProvider.fallback() |
Universal safe set: Roboto, Arial, Times New Roman, Courier New, Georgia, Verdana, Helvetica |
NovidentFontProvider.fromList(fonts, {defaultFamily}) |
Custom list — you control every font the user sees |
When no fontProvider is passed to NovidentEditor, the fallback is used
automatically. This guarantees toolbar items always have fonts to display,
even on mobile where system font enumeration is not available.
Use the system_fonts package (in your own app — not bundled with the
library) to enumerate installed fonts on desktop:
// example/ — not in the library itself
import 'package:system_fonts/system_fonts.dart';
final systemFonts = SystemFonts();
final fontList = await systemFonts.getFontList();
NovidentEditor(
editorState: editorState,
fontProvider: NovidentFontProvider.fromList(
fontList,
defaultFamily: 'Arial',
),
);buildFontFamilyItem() reads the font list from editorState.fontProvider
when no explicit fontFamilies parameter is passed:
// Uses editorState.fontProvider.availableFonts
buildFontFamilyItem()
// Overrides with an explicit list
buildFontFamilyItem(fontFamilies: ['Custom Font', 'Another'])The resolved effective style (via basedOn chain) always has a non-null
fontFamily because kDefaultBaseStyle.fontFamily = 'Roboto' acts as the
root of every inheritance chain.
final styles = NovidentEditorStyles.of(context);
final effectiveStyle = styles.resolveStyle(node);
print(effectiveStyle.fontFamily); // resolved through basedOn chaineditorState.updateNode(selection, (node) => node.copyWith(
attributes: {
...node.attributes,
'styleRef': 'heading-1',
},
));editorState.formatDelta(selection, {
NovidentRichTextKeys.fontFamily: 'Georgia',
NovidentRichTextKeys.fontSize: 14.0,
});When NovidentEditorStyles is not in the widget tree (e.g., inside a
floating toolbar overlay), use editorState.editorStyles and the
resolveEffectiveToolbarStyle helper:
import 'package:novident_editor/novident_editor.dart';
final style = resolveEffectiveToolbarStyle(
context,
editorState,
node,
);