Live training: next batches coming soon. Join the waitlist:Waitlists open:Developer Training →Developer →Copilot Studio Training →Copilot Studio →

How to Build a Modern Script Editor Web Part for SharePoint Online (SPFx)

Our IT service desk lead asked for a small notice on the SharePoint home page: the desk’s hours, a live New York clock and a green or red Open/Closed status. On a classic page, I’d have dropped a Script Editor web part on it and pasted ten lines of HTML and JavaScript.

Modern pages don’t have that web part, so we’ll build our own. The finished SPFx web part gives page authors a code editor in the property pane, renders their HTML and CSS, and runs their script tags in order, in a way SharePoint’s Content Security Policy still allows.

In this tutorial, we’ll scaffold the project with Heft, add the PnP code editor control, write a small script runner, test it in the SPGuides workbench and package it. It’s a complete modern script editor web part, plus the custom script rules that decide where it can run.

Here’s what we’re building:

Modern script editor web part showing an IT Service Desk notice with a live clock and Open now status in the SharePoint workbench
Tested withVersion
SharePoint Framework (SPFx)1.23.2 with the Heft toolchain
Node.js22 LTS
React17.0.1
PnP property controls@pnp/spfx-property-controls 3.24.0 (PropertyFieldCodeEditor)

Before You Start

You need a working SPFx 1.23 setup (Node.js 22, Yeoman, the SharePoint generator and Heft) and edit rights on a SharePoint site. If your machine isn’t ready yet, follow setting up an SPFx project with the Heft toolchain first.

You also need to know one rule before you write a line of code: a web part that runs anyone’s script is a security decision, not just a coding task. Here’s what the web part has to deal with:

RuleWhat it means for a script editor
Custom script (DenyAddAndCustomizePages)Blocked by default on modern team and communication sites, OneDrive and the root site. A web part marked requiresCustomScript only shows up where an admin allowed it.
Content Security Policy (CSP)Enforced on modern pages since 2026. Inline <script> elements and onclick= attributes are blocked; eval-style code still runs.
Trusted script sourcesExternal src files load only from URLs listed in the SharePoint admin center under Advanced > Script sources.

We’ll handle all three in the code, so the web part works today and doesn’t break on the next policy change. All of it lives in our SharePoint Framework tutorials series too.

Step 1: Create the SPFx React Web Part Project

Open a terminal (I use the one inside Visual Studio Code), create a folder and run the SharePoint generator. New to SPFx? Building your first SPFx client-side web part explains every generated file:

md spg-script-editor
cd spg-script-editor
yo @microsoft/sharepoint

Answer the prompts like this:

  1. What is your solution name? spg-script-editor
  2. Which type of client-side component to create? WebPart
  3. What is your Web part name? ScriptEditor
  4. Which template would you like to use? React

If you script your setups, the same answers fit on one line:

yo @microsoft/sharepoint --solution-name spg-script-editor --component-type webpart --component-name ScriptEditor --framework react --environment spo

The generator installs the packages for you (about 5 minutes here). Older posts and projects used gulp serve; since SPFx 1.22 the toolchain is Heft, so new projects get npm run start and npm run build. Have an old gulp project? See migrating from gulp to Heft.

Checkpoint: package.json lists @microsoft/sp-webpart-base 1.23.2 and react 17.0.1, and the src/webparts/scriptEditor folder exists.

Step 2: Install the Code Editor Control and the SPFx Loader

Two packages do the heavy lifting. @pnp/spfx-property-controls gives us PropertyFieldCodeEditor, a full code editor with HTML syntax highlighting inside the property pane. @microsoft/sp-loader gives us SPComponentLoader for external script files:

npm install @pnp/spfx-property-controls@3.24.0 @microsoft/sp-loader@1.23.2 --save-exact

Version 3.24.0 is the current release of the property controls and is built against SPFx 1.23 and React 17. I pin exact versions so the whole team builds the same bundle. Adding custom controls to the SPFx property pane shows the other controls in that package.

Then add a utils folder under src/webparts/scriptEditor. Here’s the finished file tree in VS Code:

VS Code Explorer showing the SPFx script editor web part project with components, loc and utils folders

Step 3: Write the Script Runner

Here’s the core problem. When you set innerHTML, the browser shows the HTML and applies <style> blocks, but it never runs <script> tags. The old trick was to copy each one into a new script element, and SharePoint’s CSP now blocks exactly that.

So we run each script ourselves, in order. Create src/webparts/scriptEditor/utils/runScripts.ts:

import { SPComponentLoader } from '@microsoft/sp-loader';

const JS_TYPES: string[] = ['', 'text/javascript', 'application/javascript'];

/**
 * Runs the <script> tags inside a container one after another, in the order
 * they appear in the pasted code. Browsers never run scripts added with
 * innerHTML, so we pull each one out and run it ourselves.
 * Returns one message per script that failed (empty array = all good).
 */
export async function runScripts(container: HTMLElement): Promise<string[]> {
  const errors: string[] = [];
  const scripts: HTMLScriptElement[] = Array.from(container.querySelectorAll('script'));

  for (const script of scripts) {
    const type: string = (script.getAttribute('type') || '').trim().toLowerCase();
    if (JS_TYPES.indexOf(type) === -1) {
      continue; // JSON, templates and other data blocks stay as they are
    }

    script.remove(); // the inert copy is no longer needed in the page

    try {
      if (script.src) {
        // External file: SharePoint's Content Security Policy only allows it
        // when its URL is listed in Trusted script sources.
        await SPComponentLoader.loadScript(script.src);
      } else if (script.text.trim()) {
        // Inline code: CSP blocks inline <script> elements on modern pages,
        // but 'unsafe-eval' stays allowed, so we run the code as a function.
        // eslint-disable-next-line no-new-func
        const run = new Function(script.text);
        run.call(window);
      }
    } catch (error) {
      const name: string = script.src || 'inline script';
      errors.push(`${name}: ${(error as Error).message}`);
    }
  }

  return errors;
}
runScripts.ts open in VS Code with the Function constructor and SPComponentLoader.loadScript calls

Here’s what it does:

  • Order: the for...of loop with await runs one script at a time, so a library loaded by the first tag is ready before the second tag uses it.
  • Inline code: new Function(script.text) turns the code into a function and calls it. Microsoft’s CSP keeps 'unsafe-eval', so this runs where an inline <script> element is refused.
  • External files: SPComponentLoader.loadScript(url) loads a src file and waits for it. The URL still has to be a trusted script source.
  • Data blocks: tags with another type, such as application/json, are skipped and stay in the page.
  • Errors: a broken script doesn’t stop the rest; its message goes into the returned array.

One side effect to know: var and function declarations stay inside that function. If two snippets on a page must share something, put it on window on purpose.

Checkpoint: The project still compiles with no TypeScript or lint errors. Nothing renders yet.

Step 4: Build the React Component

First, replace the props in src/webparts/scriptEditor/components/IScriptEditorProps.ts:

import { DisplayMode } from '@microsoft/sp-core-library';

export interface IScriptEditorProps {
  /** HTML, CSS and JavaScript pasted by the page author */
  script: string;
  /** Optional heading shown above the output */
  title: string;
  /** Read or Edit mode of the page */
  displayMode: DisplayMode;
  /** Opens the property pane from the placeholder button */
  onConfigure: () => void;
}

Then replace src/webparts/scriptEditor/components/ScriptEditor.tsx:

import * as React from 'react';
import { DisplayMode } from '@microsoft/sp-core-library';
import { MessageBar, MessageBarType, PrimaryButton } from '@fluentui/react';
import styles from './ScriptEditor.module.scss';
import type { IScriptEditorProps } from './IScriptEditorProps';
import { runScripts } from '../utils/runScripts';

const ScriptEditor: React.FC<IScriptEditorProps> = (props) => {
  const { script, title, displayMode, onConfigure } = props;
  const hostRef = React.useRef<HTMLDivElement>(null);
  const [errors, setErrors] = React.useState<string[]>([]);
  const isEditMode: boolean = displayMode === DisplayMode.Edit;

  React.useEffect(() => {
    const host = hostRef.current;
    if (!host) {
      return;
    }

    let cancelled = false;
    host.innerHTML = script || ''; // HTML and <style> blocks show up right away

    runScripts(host)
      .then((result) => { if (!cancelled) { setErrors(result); } })
      .catch((error: Error) => { if (!cancelled) { setErrors([error.message]); } });

    return () => {
      cancelled = true;
      host.innerHTML = ''; // clear the old output before the next run
    };
  }, [script]);

  return (
    <section className={styles.scriptEditor}>
      {title && <h2 className={styles.title}>{title}</h2>}

      {!script && isEditMode && (
        <div className={styles.placeholder}>
          <p>Paste your HTML, CSS and JavaScript in the property pane.</p>
          <PrimaryButton text="Edit code" iconProps={{ iconName: 'Code' }} onClick={onConfigure} />
        </div>
      )}

      {isEditMode && errors.map((message) => (
        <MessageBar key={message} messageBarType={MessageBarType.error} isMultiline={true}>
          {message}
        </MessageBar>
      ))}

      <div ref={hostRef} />
    </section>
  );
};

export default ScriptEditor;
  • The host div: React renders an empty <div> and never touches its children, so our innerHTML and React don’t fight.
  • The effect: every time the code changes, it writes the HTML and runs the scripts. The cleanup empties the div first, so an edit doesn’t leave two copies behind.
  • Edit mode only: the placeholder with the Edit code button and the red MessageBars only appear while the page is being edited. Readers never see script errors.

The styles are small. Replace ScriptEditor.module.scss with the following; the [theme:...] tokens follow the site theme, and Fluent UI in SPFx covers the components we used:

.scriptEditor {
  color: "[theme:bodyText, default: #323130]";
}

.title {
  margin: 0 0 12px;
  font-size: 20px;
  font-weight: 600;
}

.placeholder {
  padding: 24px;
  text-align: center;
  border: 1px dashed "[theme:neutralTertiary, default: #a19f9d]";
  border-radius: 4px;

  p {
    margin: 0 0 12px;
  }
}

Step 5: Add the Code Editor to the Property Pane

The web part class passes the code, the title and the page mode to React, and puts PropertyFieldCodeEditor in the pane. Replace src/webparts/scriptEditor/ScriptEditorWebPart.ts:

import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
  type IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import {
  PropertyFieldCodeEditor,
  PropertyFieldCodeEditorLanguages
} from '@pnp/spfx-property-controls/lib/PropertyFieldCodeEditor';

import * as strings from 'ScriptEditorWebPartStrings';
import ScriptEditor from './components/ScriptEditor';
import { IScriptEditorProps } from './components/IScriptEditorProps';

export interface IScriptEditorWebPartProps {
  title: string;
  script: string;
}

export default class ScriptEditorWebPart extends BaseClientSideWebPart<IScriptEditorWebPartProps> {

  public render(): void {
    const element: React.ReactElement<IScriptEditorProps> = React.createElement(
      ScriptEditor,
      {
        title: this.properties.title,
        script: this.properties.script,
        displayMode: this.displayMode,
        onConfigure: () => this.context.propertyPane.open()
      }
    );

    ReactDom.render(element, this.domElement);
  }

  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('title', {
                  label: strings.TitleFieldLabel
                }),
                PropertyFieldCodeEditor('script', {
                  label: strings.ScriptFieldLabel,
                  panelTitle: strings.ScriptPanelTitle,
                  initialValue: this.properties.script,
                  onPropertyChange: this.onPropertyPaneFieldChanged,
                  properties: this.properties,
                  disabled: false,
                  key: 'scriptCodeEditor',
                  language: PropertyFieldCodeEditorLanguages.HTML,
                  options: {
                    wrap: true,
                    fontSize: 14
                  }
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

PropertyFieldCodeEditorLanguages.HTML switches on HTML highlighting, which also colors the CSS and JavaScript inside it. The editor opens in a side panel and saves the code into the script property. Configurable properties in the SPFx property pane explains how those properties are stored with the page.

Update the label names in src/webparts/scriptEditor/loc/mystrings.d.ts:

declare interface IScriptEditorWebPartStrings {
  PropertyPaneDescription: string;
  BasicGroupName: string;
  TitleFieldLabel: string;
  ScriptFieldLabel: string;
  ScriptPanelTitle: string;
}

declare module 'ScriptEditorWebPartStrings' {
  const strings: IScriptEditorWebPartStrings;
  export = strings;
}

And the English strings in src/webparts/scriptEditor/loc/en-us.js:

define([], function() {
  return {
    "PropertyPaneDescription": "Add HTML, CSS and JavaScript to this page. Scripts run with the permissions of the person viewing the page.",
    "BasicGroupName": "Script Editor",
    "TitleFieldLabel": "Title (optional)",
    "ScriptFieldLabel": "HTML, CSS and JavaScript",
    "ScriptPanelTitle": "Edit the code"
  }
});

Step 6: Set requiresCustomScript in the Manifest

Open ScriptEditorWebPart.manifest.json. Set requiresCustomScript to true, keep the web part to SharePoint pages only and give it a friendly toolbox entry. Configuring preconfiguredEntries explains each field:

"requiresCustomScript": true,
"supportedHosts": ["SharePointWebPart"],
"supportsThemeVariants": true,

"preconfiguredEntries": [{
  "groupId": "5c03119e-3074-46fd-976b-c60198311f70",
  "group": { "default": "Advanced" },
  "title": { "default": "Script Editor" },
  "description": { "default": "Add your own HTML, CSS and JavaScript to a modern page" },
  "officeFabricIconFontName": "Code",
  "properties": {
    "title": "",
    "script": ""
  }
}]

requiresCustomScript: true tells SharePoint this web part lets authors add script. Microsoft’s custom script rules then apply to it like they did to the classic Script Editor:

  • On a site where custom script is blocked, the web part isn’t available in the web part picker.
  • Instances added while custom script was allowed stop showing on their pages once it’s blocked again.
  • Custom script is blocked by default on modern team sites, communication sites, OneDrive and the root site.

That’s the honest setting for a script editor. Set it to false and the web part shows up on every site, so any page editor in the tenant can run script as every visitor. On the SPGuides site, DenyAddAndCustomizePages reads Enabled, which means custom script is blocked.

One thing surprised me: the workbench still listed the web part on that site. The workbench loads your local debug manifest, so the custom script check that hides the deployed web part doesn’t kick in there. Test the real rule after deployment, not in the workbench.

Checkpoint: Saving every file gives no errors, and the toolbox entry will read Script Editor with a code icon.

Step 7: Run It in the SPGuides Workbench with heft start

Point the local server at your own site’s workbench. Open config/serve.json and set initialPage:

{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
  "port": 4321,
  "https": true,
  "initialPage": "https://{tenantDomain}/sites/SPGuides/_layouts/workbench.aspx"
}

Heft swaps {tenantDomain} for the SPFX_SERVE_TENANT_DOMAIN environment variable; SPFx environment variables shows how to set it for good. Then start the local server from the project folder:

# PowerShell, current terminal session
$env:SPFX_SERVE_TENANT_DOMAIN = "spguides.sharepoint.com"
npm run start

Heft builds the project, serves it from https://localhost:4321, prints the debug query string and opens the workbench:

heft start in the VS Code terminal serving the SPFx script editor web part on localhost 4321 with the debug query string
  1. In the workbench, select the + (add a new web part) button.
  2. Search for Script Editor and select it.
  3. The web part shows the dashed placeholder with an Edit code button.
SharePoint workbench web part toolbox search showing the Script Editor SPFx web part
Empty modern script editor web part in edit mode with the Edit code button

One heads-up: Microsoft retires the hosted workbench on 12/01/2026. After that, keep heft start running, open a real page, add ?debugManifestsFile=https%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fbuild%2Fmanifests.js&debug=true&noredir=true to its URL and select Load debug scripts.

Test the Script Editor Web Part

Now the fun part. Select Edit code (or the pencil icon) to open the property pane, then select the code editor button under HTML, CSS and JavaScript:

SPFx script editor property pane with the Title field and the PropertyFieldCodeEditor button

Type a title such as IT Service Desk; it appears above the output right away.

Paste the service desk snippet into the editor panel and save it. It has a style block, a little HTML and one script:

<style>
  .spg-notice { border-left: 6px solid #0f6cbd; background: #eef6fc; padding: 16px 20px; border-radius: 4px; }
  .spg-notice h3 { margin: 0 0 8px; color: #0f6cbd; font-size: 18px; }
  .spg-notice p { margin: 4px 0; }
  .spg-clock { font-size: 26px; font-weight: 600; }
  .spg-open { color: #107c10; font-weight: 600; }
  .spg-closed { color: #c50f1f; font-weight: 600; }
</style>

<div class="spg-notice">
  <h3>IT Service Desk hours</h3>
  <p>We're here Monday to Friday, 8:00 AM to 6:00 PM Eastern Time.</p>
  <p>Time in New York: <span id="spg-clock" class="spg-clock">--:--:--</span></p>
  <p>Status: <span id="spg-status">checking...</span></p>
</div>

<script>
  var clock = document.getElementById('spg-clock');
  var status = document.getElementById('spg-status');

  function tick() {
    if (!document.body.contains(clock)) { clearInterval(timer); return; } // web part removed or edited
    var now = new Date();
    clock.textContent = now.toLocaleTimeString('en-US', { timeZone: 'America/New_York' });

    var day = now.toLocaleDateString('en-US', { timeZone: 'America/New_York', weekday: 'short' });
    var hour = Number(now.toLocaleString('en-US', { timeZone: 'America/New_York', hour: 'numeric', hourCycle: 'h23' }));
    var open = ['Sat', 'Sun'].indexOf(day) === -1 && hour >= 8 && hour < 18;
    status.textContent = open ? 'Open now' : 'Closed - leave a ticket and we will reply next business day';
    status.className = open ? 'spg-open' : 'spg-closed';
  }

  var timer = setInterval(tick, 1000);
  tick();
</script>
PnP PropertyFieldCodeEditor panel with the HTML, CSS and JavaScript of the service desk notice

Select Save and close the pane. The notice picks up its styles, the clock ticks every second in Eastern Time, and the status follows the desk hours. I tested it at 9:42 PM on a Saturday, so it showed the red Closed message.

The script checks that its clock is still on the page every second, so removing or re-editing the web part stops the old timer instead of leaving it running.

Modern script editor web part rendering the styled notice with the live New York clock in the SharePoint workbench

Inline onclick Doesn’t Work Anymore: Use addEventListener

Most old Script Editor snippets wire buttons up with an attribute like this one:

<button onclick="alert('Hello from the classic Script Editor!')">Old way: onclick</button>

Under SharePoint’s Content Security Policy, an inline event handler counts as inline script. In the SPGuides workbench, the button did nothing, and the browser reported a script-src-attr violation with the disposition enforce. Our runner can’t help, because the browser blocks the attribute itself.

Wire the button up from a script block instead:

<!-- Classic Script Editor style: inline onclick attribute (blocked by CSP on modern pages) -->
<button type="button" onclick="alert('Hello from the classic Script Editor!')">Old way: onclick</button>

<!-- Modern way: same button wired up from a script block -->
<button type="button" id="spg-hello">New way: addEventListener</button>
<p id="spg-hello-result"></p>

<script>
  document.getElementById('spg-hello').addEventListener('click', function () {
    document.getElementById('spg-hello-result').textContent = 'Clicked at ' + new Date().toLocaleTimeString('en-US');
  });
</script>
Script editor web part test with the onclick button blocked and the addEventListener button showing the clicked time

The second button wrote the click time under the buttons, as expected. I also checked the old re-insert trick: a script element created with inline text was refused with a script-src-elem violation. The same goes for javascript: links and document.write.

When a Script Fails

Typos happen. To see what the web part does with one, I pasted two script blocks: the first has a stray +, the second sets the same text correctly:

<p>Office closed: <span id="spg-holiday"></span></p>

<script>
  var holiday = 'Thanksgiving, 11/26/2026'
  document.getElementById('spg-holiday').textContent = holiday +;
</script>

<script>
  document.getElementById('spg-holiday').textContent = 'Thanksgiving, 11/26/2026';
</script>

In edit mode, the web part shows inline script: Unexpected token ‘;’ in a red bar and still runs the second script, so the holiday line appears. In preview and view mode, the bar is hidden.

Script editor web part edit mode MessageBar showing a JavaScript syntax error from an inline script

The Classic Script Editor and Content Editor Web Parts (and Why Modern Pages Don’t Have Them)

If you came here from an older SharePoint 2013, 2016 or classic SharePoint Online site, this part’s for you. Classic web part pages had two ways to add code:

  • Script Editor (Media and Content category): select Edit Snippet, paste HTML, CSS or JavaScript, and it ran on the page.
  • Content Editor: rich text, or a link to an .html or .txt file in a library that held the code.

Modern pages never got either of them. Microsoft designed modern pages around SPFx web parts that an admin approves once in the app catalog, instead of code that any page editor can paste. For plain styling, adding custom HTML and CSS to a modern page covers the no-code options.

Classic pages still have both web parts, but only where custom script is allowed. An admin can allow it for one site:

# SharePoint Online Management Shell, as a SharePoint Administrator
Connect-SPOService -Url "https://spguides-admin.sharepoint.com"
Set-SPOSite "https://spguides.sharepoint.com/sites/SPGuides" -DenyAddAndCustomizePages 0

# Same thing with PnP PowerShell
Set-PnPSite -Identity "https://spguides.sharepoint.com/sites/SPGuides" -NoScriptSite $false

Older posts (including the first version of this one) said that’s all it takes. It isn’t anymore: Microsoft now resets the setting to Not allowed within 24 hours, and you can’t allow it on OneDrive at all. The SharePoint admin center shows the same switch under Active sites > a site > Settings > Custom scripts.

SharePoint Server 2016 and 2019 on-premises still include both classic web parts on classic web part pages. SharePoint Online is where the rules got strict.

Content Editor Web Part in SharePoint Online: What to Use on Modern Pages

On a modern page, you can’t get the Content Editor or Script Editor back, even with custom script allowed. Two separate rules are at work, and both apply to modern pages only.

  • Custom script setting: when a site has DenyAddAndCustomizePages turned on (the default for modern team and communication sites), the classic Content Editor, Script Editor and Embed web parts disappear from classic pages too.
  • Content Security Policy: since March 1, 2026, SharePoint Online enforces CSP on modern pages. Inline script added by any script editor won’t run, although its HTML still renders. Tenants could delay enforcement only until June 1, 2026.

CSP isn’t enforced on classic pages, so an old classic page on a site that allows custom script still runs its Content Editor code. Treat that as a leftover to migrate, not a place to add new code.

Here’s what replaces each classic job on a modern page:

  • Formatted text, tables and links: the built-in Text web part.
  • A video, map, form or app from another site: the modern Embed web part (or a dedicated one, like when you embed Power Apps in a SharePoint modern page). It takes a URL or iframe embed code, and only from domains your site allows in HTML Field Security. Script tags aren’t accepted.
  • Pasted HTML and JavaScript: the SPFx script editor built in this post, or the PnP sample below. Put any script logic in script blocks, not inline handlers.
  • Code every page needs: an SPFx Application Customizer, deployed once from the app catalog.

The PnP Modern Script Editor Sample

You don’t have to build this yourself. The PnP community has kept a Script Editor web part for modern pages in the sp-dev-fx-webparts repository for years (folder react-script-editor). It’s the one most admins mean when they say “the modern script editor”.

Our spg-script-editorPnP react-script-editor
SPFx / toolchain1.23.2 with Heft1.20.1, gulp-based build steps in its readme
Code editorPropertyFieldCodeEditor in a side panelIts own editor
Inline scripts under CSPFunction constructorFunction constructor since version 1.0.27.0 (November 2025)
ExtrasKept small on purposeTeams tab support, remove-padding option, classic _spPageContextInfo
requiresCustomScripttruetrue by default; its readme shows how to set false

Pick the PnP sample when you want features today; build your own when you want code you can read in ten minutes and keep current with regular SPFx version updates. Either way, the same custom script and CSP rules apply.

Make It Production-Ready

  • Decide who can use it: keep requiresCustomScript: true, and allow custom script only on the sites that truly need it. Every script runs with the permissions of whoever views the page.
  • Prefer a real web part: if the same snippet lands on ten pages, turn it into its own SPFx web part. A small build like the SPFx countdown timer web part is easier to review than pasted code.
  • Trusted script sources: list exact URLs (with the version) for any library a snippet loads. Wildcards like * and 'unsafe-inline' aren’t accepted.
  • Unique IDs: two copies of a snippet on one page share element IDs. Prefix them or scope the code to its own container.
  • Site-wide code: CSS or JavaScript every page needs belongs in an SPFx Application Customizer, not in a web part on each page.

Troubleshooting

  • Script Editor isn’t in the toolbox on a real page: custom script is blocked on that site and the manifest says requiresCustomScript: true. That’s the rule working; ask a SharePoint admin. The workbench still shows it, which is why it looks fine during development.
  • HTML shows but nothing happens: the snippet uses onclick=, javascript: links or document.write. Look for a Content Security Policy message in the browser console and move the code into a script block.
  • “Refused to load the script” for a library: the src URL isn’t in Trusted script sources. Add the exact file URL in the SharePoint admin center or bundle the library with the web part.
  • The clock runs twice as fast after an edit: an old timer is still running. Clear it when its element leaves the page, as the demo snippet does.
  • The web part isn’t in the workbench at all: heft start stopped or is still compiling. Wait for the debug query string line in the terminal, then refresh.

Package the Web Part for Production

Once it behaves the way you want, stop heft start with Ctrl+C and build the production package:

npm run build

The script runs heft test --clean --production and then heft package-solution --production. It took about 2 minutes (114 seconds on the first run) here and wrote sharepoint/solution/spg-script-editor.sppkg:

npm run build in the VS Code terminal creating the spg-script-editor.sppkg SPFx package

That file is ready to ship: upload the .sppkg to the SharePoint app catalog when you’re ready, following Microsoft Learn: Deploy your client-side web part.

Where to Take It Next

  • No React at all: the same runner works in a No Framework web part; set this.domElement.innerHTML and call runScripts(this.domElement) in render(). SPFx with no JavaScript framework shows that project type.
  • A nicer icon: swap the Code icon for your own image with a custom SPFx web part icon.
  • Show another page instead: if the goal is to show another site, the Page Viewer web part or the Embed web part may be all you need.

If you’d like this web part, and many more SPFx samples, as ready-to-run projects, they’re on the Power Platform and SPFx solutions page.

More SharePoint Framework builds that go well with this one:

Frequently Asked Questions

Is there a script editor web part for modern SharePoint pages?

Not out of the box. Modern pages have no Script Editor or Content Editor web part. You can build an SPFx web part like the one in this post, or deploy the PnP community react-script-editor sample from the sp-dev-fx-webparts repository.

Why is my modern script editor web part missing from the toolbox?

Its manifest sets requiresCustomScript to true and custom script is blocked on that site, which is the default for modern team and communication sites. Microsoft Learn: Allow or prevent custom script explains the setting and its 24-hour reset.

How do I enable the Script Editor web part in SharePoint Online?

A SharePoint admin allows custom script for the site with Set-SPOSite -DenyAddAndCustomizePages 0, Set-PnPSite -NoScriptSite $false, or the Custom scripts setting in the admin center. SharePoint Online resets it to Not allowed within 24 hours.

Does SharePoint’s Content Security Policy block script editor web parts?

It blocks inline script elements, inline event handlers and javascript: links on modern pages, so a web part that re-adds script tags stops working. Running the code with the Function constructor still works, and external files need a Trusted script sources entry.

What replaced the Content Editor web part in SharePoint Online?

The Text web part covers formatted text, the Embed web part shows iframes from allowed sites, and SPFx web parts or Application Customizers handle custom code. The classic Content Editor only works on classic pages where custom script is allowed.