How Read Frog Manages Translation Node Styling: Presets and Custom CSS Integration
Read Frog manages translation node styling through a dual-layer system that combines eight predefined visual presets with optional custom CSS, using Zod-schematized configuration and runtime style injection via adoptedStyleSheets or fallback style elements.
Read Frog, an open-source browser extension for webpage translation, provides flexible translation node styling that balances ease of use with deep customization. The system defined in the mengxi-ream/read-frog repository allows users to select from preset visual themes or inject their own CSS rules, ensuring translated content integrates seamlessly with any website's design through configuration stored in src/types/config/translate.ts and runtime injection logic in src/utils/host/translate/ui/style-injector.ts.
Translation Node Style Configuration Schema
The styling configuration is strictly typed using Zod schemas defined in src/types/config/translate.ts. This ensures type safety across the extension's options page and content scripts.
The core configuration object follows this structure:
export const translationNodeStylePresetSchema = z.enum(TRANSLATION_NODE_STYLE)
export type TranslationNodeStylePreset = z.infer<typeof translationNodeStylePresetSchema>
export const translationNodeStyleConfigSchema = z.object({
preset: translationNodeStylePresetSchema,
isCustom: z.boolean(),
customCSS: z.string().max(MAX_CUSTOM_CSS_LENGTH).nullable(),
})
preset: Stores one of the predefined constants from theTRANSLATION_NODE_STYLEarrayisCustom: Boolean toggle that switches between preset mode and custom CSS modecustomCSS: User-provided stylesheet string with a maximum length of 8 KB (enforced byMAX_CUSTOM_CSS_LENGTH)
Preset Style System and Constants
Read Frog ships with eight built-in visual presets defined in src/utils/constants/translation-node-style.ts. These presets provide immediate visual differentiation for translated text without requiring CSS knowledge.
export const DEFAULT_TRANSLATION_NODE_STYLE = "default"
export const TRANSLATION_NODE_STYLE_ON_INSTALLED = "textColor"
export const TRANSLATION_NODE_STYLE = [
DEFAULT_TRANSLATION_NODE_STYLE,
"blur",
"blockquote",
"weakened",
"dashedLine",
"border",
"textColor",
"background",
] as const
Available preset styles include:
- default: Standard formatting without special visual treatment
- blur: Applies a blur effect to indicate translated content
- blockquote: Styles translated text as a block quotation
- weakened: Reduces visual prominence of the translation
- dashedLine: Underlines translated content with a dashed border
- border: Surrounds translated nodes with a solid border
- textColor: Changes text color (used as the default for new installations)
- background: Applies a background color highlight
These constants populate the UI dropdown and drive the CSS classes injected at runtime.
User Interface for Style Selection
The options page provides two distinct interfaces for managing translation node styling, both located in src/entrypoints/options/pages/translation/custom-translation-style/.
Preset Style Selector
The Preset Style Selector component (preset-style-selector.tsx) renders a dropdown populated from the TRANSLATION_NODE_STYLE constant. When users select a preset, the component merges the selection into the global configuration atom:
<Select
value={translationNodeStyle.preset}
onValueChange={(preset) => {
if (!preset) return
void setTranslateConfig(
deepmerge(translateConfig, { translationNodeStyle: { preset } })
)
}}
>
Custom CSS Editor
When users enable custom styling mode, the CSS Editor component (css-editor.tsx) provides a text area for arbitrary CSS input. The component persists user input back to the configuration store:
await setTranslateConfig(
deepmerge(translateConfig, {
translationNodeStyle: {
...translateConfig.translationNodeStyle,
customCSS: cssInput,
},
})
)
This allows advanced users to override preset variables or define entirely new visual treatments.
Runtime CSS Injection and Decoration
The core runtime logic resides in src/utils/host/translate/ui/style-injector.ts and src/utils/host/translate/ui/decorate-translation.ts. These utilities handle stylesheet injection into both standard Document contexts and Shadow DOM environments.
Preset Style Injection
The ensurePresetStyles function bundles host-theme.css (CSS variables) and custom-translation-node.css (preset class definitions) into a single string (FULL_PRESET_CSS). It injects styles using:
adoptedStyleSheetswhen the host environment supports Constructable Stylesheets- Fallback to a
<style>element for older browsers
The function skips injection at the Document root because the extension manifest already injects preset CSS globally, but actively manages styles for Shadow DOM contexts.
Custom CSS Injection
The ensureCustomCSS function in style-injector.ts first calls ensurePresetStyles to establish base variables, then injects user-provided CSS:
export async function ensureCustomCSS(root: StyleRoot, cssText: string) {
ensurePresetStyles(root) // preset vars first
// …adoptedStyleSheets or style element logic…
}
This function caches the last CSS string for each root to prevent redundant DOM operations.
Node Decoration Logic
When translating a page, decorateTranslationNode determines which stylesheet to apply:
export async function decorateTranslationNode(
translatedNode: HTMLElement,
styleConfig: TranslationNodeStyleConfig,
) {
if (translationNodeStylePresetSchema.safeParse(styleConfig.preset).error) return
const root = getContainingShadowRoot(translatedNode) ?? document
if (styleConfig.isCustom && styleConfig.customCSS) {
translatedNode.dataset[customTranslationNodeAttribute] = "custom"
await ensureCustomCSS(root, styleConfig.customCSS)
return
}
translatedNode.dataset[customTranslationNodeAttribute] = styleConfig.preset
ensurePresetStyles(root)
}
The function sets a data-read-frog-custom-translation-style attribute on the translated node—either the preset name or "custom"—enabling targeted CSS selectors in both preset and custom stylesheets.
Extending the Preset System
Developers can add new preset styles without modifying core injection logic:
- Add the preset constant in
src/utils/constants/translation-node-style.ts:
export const TRANSLATION_NODE_STYLE = [
/* existing values … */
"underline", // ← new preset
] as const
- Create CSS rules in
assets/styles/custom-translation-node.css:
[data-read-frog-custom-translation-style="underline"] {
text-decoration: underline;
text-decoration-color: var(--read-frog-primary);
}
The Preset Style Selector component automatically includes the new option because it reads directly from the TRANSLATION_NODE_STYLE constant.
Summary
- Read Frog provides eight built-in translation node style presets (
default,blur,blockquote,weakened,dashedLine,border,textColor,background) defined insrc/utils/constants/translation-node-style.ts - The configuration schema in
src/types/config/translate.tsenforces type safety using Zod, supporting both preset selection and custom CSS up to 8 KB - Runtime injection via
ensurePresetStylesandensureCustomCSSinstyle-injector.tsuses Constructable Stylesheets (adoptedStyleSheets) with<style>element fallbacks for cross-browser compatibility - Node decoration through
decorateTranslationNodeapplies thedata-read-frog-custom-translation-styleattribute, enabling scoped CSS targeting in both standard pages and Shadow DOM contexts - The system requires no code changes beyond CSS and constant additions to extend preset functionality
Frequently Asked Questions
What preset styles are available in Read Frog?
Read Frog includes eight preset styles defined in TRANSLATION_NODE_STYLE: default, blur, blockquote, weakened, dashedLine, border, textColor, and background. The textColor preset is applied by default when users first install the extension (TRANSLATION_NODE_STYLE_ON_INSTALLED), while default represents the fallback constant (DEFAULT_TRANSLATION_NODE_STYLE).
How does Read Frog handle custom CSS injection?
Custom CSS is stored in the translationNodeStyle.customCSS configuration field and injected at runtime by the ensureCustomCSS function in src/utils/host/translate/ui/style-injector.ts. The system first injects preset variables via ensurePresetStyles, then applies the user CSS using adoptedStyleSheets when available, falling back to a <style> element for broader compatibility. CSS is cached per root to avoid duplicate injections.
Can I add my own preset styles to Read Frog?
Yes. To add a new preset, append your identifier to the TRANSLATION_NODE_STYLE array in src/utils/constants/translation-node-style.ts, then add corresponding CSS rules to assets/styles/custom-translation-node.css targeting [data-read-frog-custom-translation-style="your-preset-name"]. The preset selector UI automatically populates from the constants array without requiring component modifications.
How does the extension handle translation node styling in Shadow DOM contexts?
The decorateTranslationNode function detects Shadow DOM hosts using getContainingShadowRoot(translatedNode). When a shadow boundary is detected, ensurePresetStyles and ensureCustomCSS inject stylesheets directly into the ShadowRoot rather than the global document, ensuring translated nodes within web components receive correct styling. The same data-read-frog-custom-translation-style attribute mechanism applies across both standard documents and shadow trees.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →