{"record":{"id":"32ed642bf2ceacbd","repo":"slab/quill","slug":"invalid-theme-options-theme-did-you-register-i","errorCode":null,"errorMessage":"Invalid theme ${options.theme}. Did you register it?","messagePattern":"Invalid theme (.+?)\\. Did you register it\\?","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/quill/src/core/quill.ts","lineNumber":808,"sourceCode":"  );\n}\n\nfunction expandConfig(\n  containerOrSelector: HTMLElement | string,\n  options: QuillOptions,\n): ExpandedQuillOptions {\n  const container = resolveSelector(containerOrSelector);\n  if (!container) {\n    throw new Error('Invalid Quill container');\n  }\n\n  const shouldUseDefaultTheme =\n    !options.theme || options.theme === Quill.DEFAULTS.theme;\n  const theme = shouldUseDefaultTheme\n    ? Theme\n    : Quill.import(`themes/${options.theme}`);\n  if (!theme) {\n    throw new Error(`Invalid theme ${options.theme}. Did you register it?`);\n  }\n\n  const { modules: quillModuleDefaults, ...quillDefaults } = Quill.DEFAULTS;\n  const { modules: themeModuleDefaults, ...themeDefaults } = theme.DEFAULTS;\n\n  let userModuleOptions = expandModuleConfig(options.modules);\n  // Special case toolbar shorthand\n  if (\n    userModuleOptions != null &&\n    userModuleOptions.toolbar &&\n    userModuleOptions.toolbar.constructor !== Object\n  ) {\n    userModuleOptions = {\n      ...userModuleOptions,\n      toolbar: { container: userModuleOptions.toolbar },\n    };\n  }\n","sourceCodeStart":790,"sourceCodeEnd":826,"githubUrl":"https://github.com/slab/quill/blob/539cbffd0a13b18e9c65eb84dd35e6596e403158/packages/quill/src/core/quill.ts#L790-L826","documentation":"In expandConfig, when options.theme differs from Quill.DEFAULTS.theme (the built-in 'snow'), Quill resolves the theme class via Quill.import(`themes/${options.theme}`). Quill.import returns undefined for an unregistered path and logs a debug error, and expandConfig then throws because the theme class is required to construct the editor. Quill v2 only registers the default theme; every other theme (including the built-in 'bubble') must be explicitly imported/registered.","triggerScenarios":"new Quill(el, { theme: 'bubble' }) without importing/registering the bubble theme; referencing a custom theme name that was never passed to Quill.register; a typo in the theme string; tree-shaking removing the theme module's registration side effect.","commonSituations":"Quill v1->v2 migration where bubble was bundled by default; using ESM tree-shaking builds that drop the theme import; loading only packages/quill/src/core and expecting all themes present; custom theme class authored but not registered.","solutions":["Import and register the built-in bubble theme before construction: import BubbleTheme from 'quill/themes/bubble'; Quill.register('themes/bubble', BubbleTheme);.","For a custom theme class, register it: Quill.register('themes/my-theme', MyTheme) (or rely on its side-effect import).","Double-check the theme string spelling matches a registered path exactly (themes/<name>).","If you do not need a custom theme, omit the theme option to fall back to the always-registered default 'snow' theme."],"exampleFix":"// before - bubble theme never registered\nnew Quill(el, { theme: 'bubble' }); // throws: Invalid theme bubble\n\n// after - register the theme first\nimport { Quill } from 'quill';\nimport BubbleTheme from 'quill/themes/bubble';\nQuill.register('themes/bubble', BubbleTheme);\nnew Quill(el, { theme: 'bubble' });","handlingStrategy":"validation","validationCode":"// Confirm the theme is registered before constructing Quill\nimport { Quill } from 'quill';\n\nfunction assertThemeRegistered(themeName) {\n  const isDefault = !themeName || themeName === Quill.DEFAULTS.theme;\n  if (isDefault) return; // 'snow' is always registered\n  const theme = Quill.import(`themes/${themeName}`);\n  if (!theme) {\n    throw new Error(\n      `Theme \"${themeName}\" is not registered. Import it and call Quill.register('themes/${themeName}', ThemeClass).`,\n    );\n  }\n}\n\n// usage\nassertThemeRegistered(options.theme);\nnew Quill(el, options);","typeGuard":"function isThemeRegistered(themeName) {\n  if (!themeName || themeName === Quill.DEFAULTS.theme) return true;\n  return Quill.import(`themes/${themeName}`) != null;\n}\n\n// usage\nif (isThemeRegistered('bubble')) {\n  new Quill(el, { theme: 'bubble' });\n}","tryCatchPattern":"try {\n  new Quill(el, { theme });\n} catch (err) {\n  if (String(err?.message).startsWith('Invalid theme')) {\n    // theme not registered - fall back to default or register then retry once\n    Quill.register(`themes/${theme}`, FallbackThemeClass);\n    new Quill(el, { theme });\n  } else {\n    throw err;\n  }\n}","preventionTips":["Register every non-default theme via Quill.register('themes/<name>', ThemeClass) at app bootstrap.","Keep a single registration module (e.g., quill-setup.ts) imported once at app entry to avoid tree-shaking dropping side effects.","Validate the theme name resolves with Quill.import before constructing Quill.","Omit the theme option when you only need the default 'snow' theme."],"tags":["theme","registration","import","initialization","quill-v2"],"backgroundTag":null,"analyzedSha":"539cbffd0a13b18e9c65eb84dd35e6596e403158","analyzedAt":"2026-08-12T17:15:45.919Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}