---
title: SC Rich Text Enhanced
description: A CMS rich-text renderer with spacing control, variable-font axes and sanitization.
url: https://secretocho.design/lab/sc-rich-text-enhanced/
type: SoftwareSourceCode
status: working
published: 2025-08-28
updated: 2025-08-28
---

# SC Rich Text Enhanced

> A CMS rich-text renderer with spacing control, variable-font axes and sanitization.

component 11 / 28 · WORKING

- **type**: Code Component
- **platform**: Framer
- **last update**: 2025-08-28
- **read time**: 12–15 min
- **license**: SecretoCho Design

![A still of the SC Rich Text Enhanced demo, as its module draws it with reduced motion.](https://secretocho.design/_astro/still.U1Ir-HDF_Z1rdojD.webp)

*fig 00 · live demo, ported from **SC\_RichTextEnhanced.tsx***

CMS rich-text renderer with per-section spacing CSS, optional variable-font axes, and built-in HTML sanitization to strip scripts and dangerous attributes.

A CMS-friendly rich-text renderer with precise spacing control, optional variable-font axes, scoped per-instance CSS injection, and a built-in HTML sanitizer (toggleable). Great for long-form articles, case-study bodies, and doc pages where you want consistent rhythm for paragraphs, lists, and headings.

- Content
- Typography
- CMS

## Main

### Overview

- **Built With**: React, CSS, Framer Code Component API
- **Layout Support**: Fully Responsive
- **Orientation**: Full Width Block
- **Ideal Use Case**: Editorial content, documentation, CMS blog bodies

### Tags

- rich text
- cms
- typography
- variable font
- spacing
- lists
- headings
- sanitizer
- xss

### Code comment

```tsx
/**
 * Rich Text Enhanced
 * © SecretoCho Design 2025 (https://secretocho.design)
 * CMS-friendly rich-text renderer with scoped spacing controls and optional variable-font axes.
 * Injects per-instance CSS via sectionId; includes lightweight sanitizer for untrusted HTML.
 * @framerDisableUnlink
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight any
 */
```

### Dependencies

- **Technologies**: None (pure React + inline CSS)
- **Notes**: No external libs. Styles are injected per-instance and removed on unmount.

### Complexity

- **Logic Level**: Low–Medium (style injection + guards)
- **Runtime Cost**: Very Low (static render; no heavy effects)
- **Component Size**: ~3–4 KB (raw TSX)
- **Notes**: Sanitization is lightweight; for high-risk content consider a hardened sanitizer.

## Guide

### Instructions

1. **Connect Content:** Bind a CMS Rich Text field or paste HTML/markup into **Content**.
2. **Typography:** Choose **Font** family/size/weight. Enable **Variable Font** to expose axes and set **cssFontWeight** fallback.
3. **Spacing:** Tune **Paragraph Gap**, **List Spacing** (indent, item gaps, nested spacing), and **Heading Spacing** (H1–H6 top/bottom).
4. **Appearance:** Set **Background** and **Radius** for the container block.
5. **Security:** Leave **XSS Protection** **on** for untrusted content.
6. **Multiple Instances:** Give each block a unique **Section ID** (scopes the injected CSS).

### Examples / Presets

- Article Default
  
  paragraphGap=16, listSpacing.indent=24, heading defaults.
  
- Compact Docs
  
  paragraphGap=10, listSpacing.itemGap=4, headings −25% top/bottom.
  
- Mono Notes
  
  - **variableFont**: on
  - **MONO**: 1
  - **wght**: 450; color=#0f0 on dark bg
  

## Mechanics

### How it Works

- **Scoped Styles:** A unique class from **sectionId** is generated; a \<style\> tag with calculated rules is injected and cleaned up on unmount.
- **Variable Fonts:** When enabled, the component builds a font-variation-settings string; if wght not used, falls back to **cssFontWeight**.
- **Sanitization (optional):** Strips \<script\>, inline on\* handlers, and javascript: URLs. If disabled, a subtle “XSS Protection Off” badge appears (non-interactive).
- **Rendering:** If content is a string, it is (optionally sanitized and) rendered via dangerouslySetInnerHTML; if it’s React nodes, they’re rendered directly.

### Performance & Tuning

- Most work is **one-time** (style inject). Keep **sectionId** stable to avoid unnecessary de-inject/re-inject cycles.
- Prefer **variable axes** over many separate font weights to minimize layout shifts.
- For very large articles, avoid deeply nested lists to keep layout calculations trivial.

### Compatibility & Limits

- **Browsers:** Modern evergreen (Chrome, Edge, Safari, Firefox).
- **Limits:** Built-in sanitizer is intentionally small; for highly untrusted inputs, consider pre-sanitizing at the CMS or using a hardened sanitizer upstream.
- **CMS Quirks:** Some editors output empty wrappers (\<p class="framer-text"\>\</p\>). The component filters common empties.

## Quality

### Accessibility

- Preserves semantic tags from your CMS; ensure headings remain in **logical order**.
- Links remain underlined by default; color choices must meet contrast guidelines.
- Placeholder state announces via aria-live="polite" when no content is connected.

### Security & Privacy

- **Data Flow:** No network calls; renders the content you provide.
- **Sanitizer:** Removes \<script\>, inline events, and javascript: URLs. Toggle via **XSS Protection**.
- **Risk Note:** Because dangerouslySetInnerHTML is used for string content, keep sanitization **on** for any untrusted input.

## Reference

### Props

| name | type | default | description |
| --- | --- | --- | --- |
| content | richtext/string | ”” | CMS Rich Text or HTML/JSX to render |
| sectionId | string | “section1” | Unique id to scope injected styles |
| font | font | Inter 1rem/400 | Base typography for all content |
| color | string | #111 | Text color |
| align | enum(“left”,“center”,“right”) | “left” | Paragraph alignment |
| paragraphSpacing | number | 16 | Gap between adjacent <p> blocks (px) |
| variableFont | boolean | false | Enable variable font axes |
| variableAxes | object | (MONO, CASL, CRSV, slnt, wght, cssFontWeight) | Axis values + CSS fallback |
| listSpacing | object | {indent:24,itemGap:8,blockTop:16,blockBottom:16,nestedTop:8,nestedBottom:8} | List rhythm controls |
| headingSpacing | object | {h1Top:32,h1Bottom:16,…h6Bottom:8} | Top/bottom margins per heading level |
| appearance | object | {backgroundColor:“transparent”,borderRadius:0} | Container styling |
| enableSanitization | boolean | true | Strip scripts/handlers/unsafe URLs |
| placeholderMinHeight | number | 80 | Min height for placeholder (px) |

### Notes & Tips

- Give each instance a **distinct Section ID** to prevent style bleed.
- If your brand uses a variable font, prefer axes over multiple font files for smoother weight transitions.
- Avoid pasting inline \<style\> or \<iframe\> blocks—strip them at the CMS.

### Troubleshooting / FAQ

My list spacing looks off.

Adjust **List Spacing → indent** and **itemGap**; check nested spacing values.

Headings collide with previous elements.

Increase the relevant **H# Top** spacing.

Security badge shows up.

Turn **XSS Protection** back **on** or keep it off only for trusted content.

Fonts ignore wght.

Ensure **variableFont=on** and your chosen family supports the axes; otherwise set **cssFontWeight**.

## Source

### Header comment

*SC\_RichTextEnhanced.tsx · lines 6–14 · header comment*

```tsx
/**
 * Rich Text Enhanced
 * © SecretoCho Design 2025 (https://secretocho.design)
 * CMS-friendly rich-text renderer with scoped spacing controls and optional variable-font axes.
 * Injects per-instance CSS via sectionId; includes lightweight sanitizer for untrusted HTML.
 * @framerDisableUnlink
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight any
 */
```

[props \[REF\]](#reference)

### Key excerpt

The sanitizer: parse into a detached element, remove script tags, on\* attributes and javascript: links, then serialize.

*SC\_RichTextEnhanced.tsx · lines 16–51 · tsx*

```tsx
// Basic HTML sanitizer for XSS protection (no npm needed in Framer)
function sanitizeHTML(html: string): string {
    if (!html) return ""

    // Create a temporary element to parse HTML
    const tempDiv = document.createElement("div")
    tempDiv.innerHTML = html

    // Remove script tags
    const scripts = tempDiv.getElementsByTagName("script")
    for (let i = scripts.length - 1; i >= 0; i--) {
        scripts[i].parentNode?.removeChild(scripts[i])
    }

    // Remove dangerous attributes
    const allElements = tempDiv.getElementsByTagName("*")
    for (let i = 0; i < allElements.length; i++) {
        const el = allElements[i]
        // Remove on* event handlers
        for (let j = el.attributes.length - 1; j >= 0; j--) {
            const attr = el.attributes[j]
            if (attr.name.startsWith("on")) {
                el.removeAttribute(attr.name)
            }
        }
        // Remove javascript: URLs
        if (el.hasAttribute("href")) {
            const href = el.getAttribute("href")
            if (href && href.trim().toLowerCase().startsWith("javascript:")) {
                el.setAttribute("href", "#")
            }
        }
    }

    return tempDiv.innerHTML
}
```

## Credits

### Credits

Authored by **SecretoCho Design**. Design patterns informed by common editorial systems and variable-font practices.

- **license**: SecretoCho Design

SecretoCho Design 2025

### Disclaimer

Provided “as is” for content/UX rendering. You’re responsible for validating accessibility, performance, and content safety for your project and data sources.

## Proves

- [Creative coding](https://secretocho.design/about/#capabilities)

## Hiring a design engineer?

I’m open to senior design engineer roles: remote from Belgrade, or relocation with visa sponsorship. Write to me, or read the CV first. Client work stays selective, one project at a time.

[sc@secretocho.design](mailto:sc@secretocho.design)

[read the CV](https://secretocho.design/cv/) · [LinkedIn](https://www.linkedin.com/in/secretocho/)
