0020. A disabled editor draws the disabled box of the theme's inputs¶
Status: Accepted
Date: 2026-09-19
Context¶
0013 gave the editable a box: a border, a radius, block padding and a minimum height, so it reads as a form control beside the other fields on the form. That record left one case open. A textarea can be disabled after the editor has already attached, which is what a #states rule does, and what any module altering the form can do at any point; the editor's own watchEditable() (js/src/index.tsx) observes the textarea's disabled and readonly attributes and marks the editable aria-readonly="true" when either is set, through markReadOnly(). Nothing changes the box. An author looking at the form sees a bordered, white-backgrounded control that looks exactly like the one beside it they can still type into, and no sighted cue says otherwise: the box 0013 gave the editor does not distinguish an editable state from a disabled one.
Every admin theme this module supports draws a disabled <input> or <textarea> differently from an editable one. Claro's .form-element[disabled] (css/components/form--text.pcss.css) sets color: var(--input--disabled-fg-color), border-color: var(--input--disabled-border-color) and background-color: var(--input--disabled-bg-color), tokens declared at :root in css/base/variables.pcss.css: --input--disabled-bg-color: #f2f2f3, --input--disabled-border-color: #bababf, --input--disabled-fg-color: var(--color-gray-800), which resolves to #55565b. Gin 5.0.15's .form-element[disabled] (dist/css/base/gin.css) sets color: var(--gin-color-disabled), background-color: var(--gin-color-disabled-bg) !important and border-color: var(--gin-color-disabled-border), declared at :root, --gin-color-disabled: #767676, --gin-color-disabled-bg: #eaeaea, --gin-color-disabled-border: #c2c2c2 (dist/css/theme/variables.css), and redeclared under .gin--dark-mode with #919191, #47474c and #646464. Both sets of tokens are declared at the root of the page, not scoped to .form-element[disabled] itself, so an element inherits them wherever in the page it sits; the editable does not need to be a descendant of anything carrying them.
0013 considered this and declined it: "A disabled appearance for an editor whose textarea is disabled through #states. Not taken for 1.0." 0013 also declined reading a theme's own tokens for the box's colors generally, on the grounds that doing so is the mirror image of what the isolation stylesheet exists to stop, an admin theme's value deciding what the editor looks like. It narrowed that decision once already, for the radius: --blocknote-editor-radius reads --gin-border-m and --input-border-radius-size before falling back to the module's own 2px, because a radius that differs from the fields beside it shows on every form, whatever that form is doing. The same argument applies here, more directly: a page that disables the field through #states puts every other input on that form into its disabled appearance at once, and an editor that keeps its editable appearance is the one field on the page that looks wrong.
WCAG 1.4.3 (Contrast (Minimum)) and 1.4.11 (Non-text Contrast) both exempt inactive user interface components from their contrast requirements, so a disabled box reading below either ratio is not itself a failure; what would be a defect is a box that fails to read as disabled at all.
Decision¶
The disabled box is drawn on aria-readonly. .blocknote-editor .bn-editor[aria-readonly="true"] sets color, border-color, background and box-shadow: none, from three new custom properties declared beside 0013's: --blocknote-editor-disabled-text, --blocknote-editor-disabled-border-color, --blocknote-editor-disabled-background. No client change was needed: markReadOnly() already writes aria-readonly from the same disabled and readonly observation 0012's read-only widget never reaches, because a read-only widget attaches no editor for watchEditable() to observe. Amended 2026-09-20. aria-readonly is written for a disabled textarea and for a read-only one alike, but only a disabled textarea is meant to draw as disabled: a read-only editor stays a tab stop, its content is still read out and still submitted, so the box above it is the wrong cue. The selector is now .blocknote-editor .bn-editor[data-blocknote-disabled="true"], a second attribute markReadOnly() writes only from element.disabled, alongside aria-readonly, and kept in step by the same watchEditable() observer.
Each property reads Gin's token, then Claro's, then the module's own fallback. --blocknote-editor-disabled-text: var(--gin-color-disabled, var(--input--disabled-fg-color, #55565b)), and the border color and background the same shape. Gin is asked first because Gin inherits Claro's tokens for everything it does not redeclare, so asking for Claro's token first would draw Claro's light gray background and border on a Gin page in dark mode, where Gin has redeclared its own darker values but Claro's have no dark variant to redeclare. The module's own fallback, reached when neither theme is present, is Claro's own light values, #f2f2f3, #bababf and #55565b, the same values the site sees if Claro is the active theme; a page with no admin theme drawing a disabled state of its own still gets a control that reads as disabled rather than the editable appearance.
Whether Gin's disabled tokens might be scoped to a selector the editable is outside of, the question this decision checked before relying on them, does not hold: both Claro's and Gin's tokens are declared at :root, and Gin's dark values under .gin--dark-mode at :root, which is an ancestor of the editable regardless of where in the page the widget renders, so the fallback chain resolves the same custom property inheritance would give a .form-element[disabled] itself.
The selector needs no :where() and loses no tie. .blocknote-editor .bn-editor[aria-readonly="true"] is two classes and the attribute the editor itself writes; nothing a theme declares for its own disabled input is written against this attribute, so there is no competing rule to outrank and no dependency on where in dist/blocknote.css the rule sits. Amended 2026-09-20. Now [data-blocknote-disabled="true"], for the reason given above; the argument is unchanged, since that attribute is written by the same code and read by nothing a theme declares either.
Alternatives considered¶
Leaving it undrawn, as 0013 did for 1.0. This is what the decision replaces. It reads correctly to a screen reader, since aria-readonly and the loss of contenteditable are both already there, but a sighted author has no visual cue that a control drawn exactly like every other field on the page cannot in fact be written to.
A color of the module's own, rather than the theme's tokens. Rejected for the reason 0013's radius amendment already gave: a color that does not match the disabled appearance of every other field the same #states rule touches would be visibly wrong on every such form, which is a worse mismatch than the one radius argument was written against, because a #states-disabled form typically disables several fields at once and puts them all into the same appearance except this one.
Styling [disabled] directly on the editable, matching a theme's own selector. Not possible. The editable is a contenteditable element with no disabled attribute or property; aria-readonly is what markReadOnly() has to write instead, so it is what the selector has to key on.
Consequences¶
A #states-disabled or otherwise runtime-disabled document field now looks disabled, in Claro and in Gin's light and dark modes, matching the rest of a form the same rule touches. Amended 2026-09-20. Computed against the editor's own background: Claro's border is 1.73:1, Gin's 1.48:1 light and 1.56:1 dark, all below the 3:1 1.4.11 asks of a component boundary; Claro's text is 6.54:1, meeting 1.4.3's 4.5:1, but Gin's is 3.78:1 light and 2.93:1 dark, below it. WCAG's exemption for an inactive control covers all of these, and is claimed only for the textarea genuinely disabled; a textarea made read-only rather than disabled keeps the editable box; see the Decision section above.
A theme that wants its own disabled appearance declares color, border-color, background or box-shadow on .blocknote-editor .bn-editor[aria-readonly="true"], or redeclares the three --blocknote-editor-disabled-* custom properties, the same override shape 0013 gives the editable box. Amended 2026-09-20. [data-blocknote-disabled="true"] now, as above.
A read-only widget is unaffected: it renders the stored document through the field formatter's own markup and attaches no editor, so nothing here is reached. Amended 2026-09-20. Nor is a textarea made read-only, rather than disabled, after the editor has already attached: aria-readonly still follows it, but the box stays the one 0013 gives every editable editor, because 1.4.3's exemption for an inactive component does not cover it, and it is not what "looks disabled" is meant to describe.
Amended 2026-09-20. A disabled editor still carries tabIndex: "0" and is announced read-only, aria-readonly, rather than disabled, aria-disabled; a screen reader user reaches it and is told what a sighted author's own eyes are not told by the same cue this record now draws. Neither the tab order nor the role changes in this pass; the mismatch is left as the state of affairs for now.
References¶
Claro 11.4.6: css/components/form--text.pcss.css, css/base/variables.pcss.css.
Gin 5.0.15: dist/css/base/gin.css, dist/css/theme/variables.css.
WCAG 2.1: 1.4.3 Contrast (Minimum), 1.4.11 Non-text Contrast.
js/src/theme-isolation.css, js/src/index.tsx (markReadOnly(), watchEditable()).
Tests: tests/src/FunctionalJavascript/AdminThemeIsolationTest.php, tests/src/FunctionalJavascript/GinAdminThemeIsolationTest.php, js/src/__tests__/theme-isolation.test.ts. Amended 2026-10-31. The Gin tests moved from AdminThemeIsolationTest into GinAdminThemeIsolationTest, so that the #[IgnoreDeprecations] a copy of Gin needs does not make the Claro tests, which CI runs, ignore deprecations.