Block Editor MVP: configurable text color palette per Story Block field
@rjvelazco is already working on this.
Since Aug 26, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
MVP for text color in the new Block Editor. A content type admin configures an explicit color palette on a Story Block field; an author can then apply only those colors to text. Nothing is exposed when no palette is configured.
This is deliberately the narrow slice out of the 8/25/26 huddle (#feat-block-editor-upgrade). Site-level cascade, design tokens (primary/secondary/accent/surface), Tailwind palette overrides, and Style Editor / UVE integration are out of scope — see Out of scope.
Storage contract
A single styleOptions field variable on the Story Block field, holding JSON. Only colors is implemented in this MVP; spacing and fontFamily are reserved in the shape so later work extends the same variable instead of adding new keys.
{
"colors": [
{ "label": "Brand Blue", "value": "#1E40AF" },
{ "label": "Accent", "value": "#F59E0B" }
],
"spacing": [],
"fontFamily": []
}
| Decision | Value | Rationale |
|---|---|---|
| Storage | fieldVariables key styleOptions |
Existing precedent — allowedBlocks, contentTypes, customBlocks. No backend model, DB, or REST contract change. |
| Format | JSON, category-keyed arrays of { label, value } |
Extensible to spacing / fontFamily without a second variable or a rename. |
| Variable key | styleOptions |
Category-agnostic; survives adding non-color options. Stored contract — renaming later breaks existing configs. |
| Author UX | Configured swatches only | The palette is a constraint, not a suggestion. Guarantees stored colors stay a known set. |
| No config | Color control hidden | Zero blast radius on existing Story Block fields. |
| Hex validity | Enforced at author-time by p-colorPicker in field settings |
The admin never types a hex value. |
Why the backend needs no change
Field variables are already a generic key/value store persisted per field (DotFieldVariablesService → field variables REST endpoint). The requested "backend key/value pair saved as a field value" is exactly what fieldVariables provides. No Java, DB, or openapi.yaml change is expected. If implementation proves otherwise, flag it on this issue before writing backend code.
Implementation anchors
| Area | Location | Note |
|---|---|---|
| Admin settings UI | core-web/apps/dotcms-ui/src/app/portlets/shared/dot-content-types-edit/components/dot-block-editor-settings/ |
Add a "Text colors" section with repeatable label + p-colorPicker rows. |
| ⚠️ Save/load serializer | dot-block-editor-settings.component.ts — saveSettings() uses value?.join(','), ngOnInit uses value.split(',') |
Blocker: every setting is assumed to be a comma-joined array. A JSON value cannot pass through unchanged — settingsMap needs a per-setting serialize/deserialize hook. |
| JSON-from-field-variable precedent | same file — getCustomBlockOptions() |
customBlocks already parses JSON out of a field variable with a try/catch + console.warn fallback. Mirror it. |
| Field variable read | core-web/libs/new-block-editor/src/lib/editor/editor.component.ts:110-130 |
parseAllowedBlocks / parseAllowedContentTypes are the pattern for a parseStyleOptions. |
| Store wiring | editor.component.ts constructor effects (~L592-602) |
Same shape as setAllowedBlocks / setAllowedContentTypes. |
| Author toolbar | core-web/libs/new-block-editor/src/lib/editor/components/toolbar/ |
New color control + EditorToolbarStore mark-state mirror. |
| TipTap dependency | core-web/package.json |
@tiptap/extension-text-style is not currently a dependency (TipTap 3.22.2). Needed for the TextStyle + Color marks. |
| Feature flag | FeaturedFlags enum, core-web/libs/dotcms-models/src/lib/shared-models.ts:44 |
New flag, e.g. FEATURE_FLAG_BLOCK_EDITOR_STYLE_OPTIONS. |
Note on TipTap node/mark names: per
core-web/libs/new-block-editor/CLAUDE.md, mark and node names are an immutable stored contract. Use TipTap's upstreamtextStylemark rather than inventing adot-prefixed one, so the stored JSON stays portable.
Acceptance Criteria
Admin — configuring the palette
- Story Block field settings shows a Text colors section with repeatable rows, each row a text
labelinput plus a PrimeNGp-colorPicker. - Admin can add a row, remove a row, and reorder is not required for this MVP.
- Saving serializes the rows to the
styleOptionsfield variable as JSON with acolorsarray of{ label, value }. -
spacingandfontFamilykeys are preserved on save when already present in the stored JSON — the color editor must not drop them. - Reopening field settings rehydrates the existing rows from the stored
styleOptionsJSON. - Removing every color row and saving clears/deletes the
styleOptionsvariable rather than storing an empty-array JSON blob. - Existing
allowedBlocks(comma-joined) save/load continues to work unchanged after the serializer refactor — covered by a spec.
Author — applying a color
- With
styleOptions.colorsconfigured, the block editor toolbar shows a color control listing exactly the configured swatches, each labelled with itslabel. - Selecting a swatch applies the color to the current text selection and it renders in the editor.
- The applied color persists in the stored TipTap JSON via the
textStylemark and survives a save → reload cycle. - A "remove color" / clear action returns the selection to inherited color.
- The toolbar control reflects the active selection's color (mirrored through
EditorToolbarStore). - Authors cannot enter an arbitrary hex — the control offers only the configured swatches.
No config / degraded input (sad path)
- Field with no
styleOptionsvariable → the color control is not rendered; no console error. -
styleOptionspresent butcolorsis missing, empty, or not an array → control hidden,console.warn, editor loads normally. - Malformed JSON in
styleOptions→ caught,console.warn, editor loads normally, no other field variable is affected. - Entry with a missing/blank
label→ falls back to its hex value as the label rather than rendering a blank swatch. - Entry with a missing or non-hex
value→ skipped; remaining valid entries still render. - Duplicate hex values with different labels → both render; no dedupe required.
Feature flag & rollout
- Whole feature sits behind a new feature flag; flag off → neither the admin section nor the toolbar control appears.
- Flag off with
styleOptionsalready stored → stored colors still render in existing content (read path is not gated), only authoring is.
Tests
- Unit specs in
core-web/libs/new-block-editor/forparseStyleOptionscovering valid, empty, malformed, and partially-invalid input. - Unit specs for the admin settings serializer covering JSON round-trip and the
allowedBlocksregression. - Unit spec for the toolbar color control: swatch list rendering, apply, clear, active-state mirroring.
Priority
Medium
Additional Context
Out of scope
Explicitly not in this MVP — each needs its own issue:
- Site-level color configuration that cascades to blocks and block editor fields.
- Semantic design tokens (primary / secondary / accent / surface) as a layer over raw hex.
- Tailwind palette structure and default-color overrides.
- Style Editor / UVE integration (
registerStyleEditorSchema). spacingandfontFamily— shape is reserved, behavior is not implemented.- Background color, and the legacy
libs/block-editor(rollback path only).
Huddle context
From the 8/25/26 huddle in #feat-block-editor-upgrade (Rafael Velazco, Freddy Montes, Adrian Molina):
- Freddy proposed design tokens as a semantic layer over raw color values, with site-level configuration cascading down to blocks and fields.
- Rafael raised concern about the blast radius of styling changes across the system — this MVP answers that by scoping config to a single field and hiding the control when unset.
- Freddy proposed starting with a simple color input in the style editor and block editor, and noted spacing and font family could extend the same mechanism later.
- Feature-flag gating was called out in the huddle as the control for whether color appears in the block editor.
- Roadmap was flagged as tight, which is the reason this ticket is a single field-scoped slice rather than the full token system.
Huddle notes were AI-generated and may contain inaccuracies.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.