v1.6.x-dev NDS IQ v6

Editor - National Design System

A rich text editor with a WYSIWYG editing surface that upgrades a standard textarea, adding a generated, localized toolbar and converting pasted Word, Google Docs, and web content into clean NDS markup while keeping pasted NDS components intact.

Rich Text Editor

A rich text editor with a WYSIWYG surface — the markup contract is a standard textarea field wearing one extra class: the editing surface, toolbar, and popovers are generated at load, and the textarea stays the form value

Default toolbar, generated from a plain textarea field
<div class="nds-form-container nds-textarea nds-editor"> <div class="nds-form-header"> <label for="editor-standard-field"><span class="nds-label">Content</span></label> </div> <div class="nds-form-control"> <textarea class="nds-textarea" name="editor-standard" id="editor-standard-field" placeholder="Write here"></textarea> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Toolbar Composition

One attribute picks the commands for the field: a comment box might ship only inline formatting while a CMS field ships everything. Server-rendered values hydrate from the textarea

Custom command set, hydrated value
<div class="nds-form-container nds-textarea nds-editor" data-editor-toolbar="bold italic underline | link | ul ol | source"> <div class="nds-form-header"> <label for="editor-composed-field"><span class="nds-label">Announcement</span></label> </div> <div class="nds-form-control"> <textarea class="nds-textarea" name="editor-composed" id="editor-composed-field" placeholder="Write here"> <h2>Portal update</h2> <p>The new services portal launches <strong>next quarter</strong>. Read the <a href="https://nds.gov.sa" rel="noopener noreferrer">full announcement</a> for details.</p> <ul> <li>Unified sign-on across services</li> <li>Faster request tracking</li> </ul> </textarea> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

NDS Components by Paste

Copy component markup from any doc page and paste it in: it stays a styled, protected component. Deletion stops at component boundaries, Enter adds a line break inside instead of splitting the structure, and the toolbar's remove button lists the component levels at the caret

Protected component shells: try deleting at the edges, Enter inside, and the remove button
<div class="nds-form-container nds-textarea nds-editor"> <div class="nds-form-header"> <label for="editor-components-field"><span class="nds-label">Composed document</span></label> </div> <div class="nds-form-control"> <textarea class="nds-textarea" name="editor-components" id="editor-components-field" placeholder="Paste component markup here"> <p>Service status: <span class="nds-tag nds-sm" data-status="success"><span class="nds-label">Active</span></span> checked daily.</p> <div class="nds-alert nds-card" data-status="info" role="alert"><span class="nds-feedback nds-alert-icon nds-outline"><span class="nds-feedback-icon"><i class="nds-icon" aria-hidden="true"></i></span></span><div class="nds-alert-content"><div class="nds-alert-text"><span class="nds-alert-title">Heads up</span><p class="nds-alert-description">This alert was pasted as markup and stays an editable, protected component.</p></div></div></div> <p>Text before and after components stays freely editable.</p> </textarea> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Images

The image popover inserts by URL, and pasted content keeps its images with safe sources, alt text, and numeric dimensions. Click an image to select it: the popover then edits it in place, the link popover wraps it in a link, and the remove button targets its component. By default no base64 enters the value — uploads appear only when a server endpoint is configured, and embedding is a per-field opt-in

Embed opt-in: try uploading, pasting a screenshot, or clicking the image
<div class="nds-form-container nds-textarea nds-editor" id="editor-images-container"> <div class="nds-form-header"> <label for="editor-images-field"><span class="nds-label">Illustrated article</span></label> </div> <div class="nds-form-control"> <textarea class="nds-textarea" name="editor-images" id="editor-images-field" placeholder="Write here"> <p>Click the image to select it, then use the toolbar's image button to edit it in place.</p> <img src="assets/img/riyadhcenter3s.webp" alt="Riyadh center" width="480"> <p>Paste a screenshot here, upload a file, or insert one from a link.</p> </textarea> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

States

Native textarea attributes drive the initial state: readonly keeps content selectable with the source view available, disabled makes the field inert and excludes it from submission. Runtime toggles go through NDS.State

Readonly
<div class="nds-form-container nds-textarea nds-editor" data-editor-toolbar="bold italic underline | source"> <div class="nds-form-header"> <label for="editor-readonly-field"><span class="nds-label">Review notes</span></label> </div> <div class="nds-form-control"> <textarea class="nds-textarea" name="editor-readonly" id="editor-readonly-field" readonly> <p>These notes are <strong>read only</strong>: content stays selectable and the source view still opens.</p> </textarea> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>
Disabled
<div class="nds-form-container nds-textarea nds-editor" data-editor-toolbar="bold italic underline | source"> <div class="nds-form-header"> <label for="editor-disabled-field"><span class="nds-label">Archived entry</span></label> </div> <div class="nds-form-control"> <textarea class="nds-textarea" name="editor-disabled" id="editor-disabled-field" disabled> <p>This entry is archived and its value does not submit.</p> </textarea> </div> <div class="nds-form-footer" data-feedback-target hidden></div> </div>

Built-in Features

Auto-initialization

Add nds-editor to a standard textarea field. The editing surface, toolbar, popovers, and form wiring are generated at load.

Generated Localized Toolbar

Commands are composed per field with one attribute, and button labels and tooltips ship in Arabic and English automatically.

Foreign Paste Conversion

Word and Google Docs content converts on paste: real lists, semantic bold and italic, tables restyled as NDS tables, junk stripped.

NDS Components by Paste

Component markup pasted from any doc page stays a styled, editable component: classes, status attributes, and sizing knobs survive while unsafe markup is stripped.

Component Shell Protection

Deletes stop at component boundaries, Enter adds a line break inside a component instead of splitting it, and removal is always an explicit action.

Image Insertion

Insert from a link, upload to your server through the embedded NDS Upload, or opt in to data:image embedding. Click any image to edit it in place, link it, or remove it.

Native Form Integration

The value lives in your textarea: it submits with the form, supports required validation, and fires input and change events like any field.

Direction-aware Alignment

Start, center, end, and justify follow text direction, so aligned content reads correctly in both RTL and LTR pages.

HTML Source View

A toolbar toggle swaps to the pretty-printed markup for direct editing, carrying your text selection over so you land where you meant to edit.

Keyboard Shortcuts

Ctrl or Cmd with B, I, and U for inline formatting, Tab nests list items, and native undo and redo work throughout.

Programmatic Control

Initialize, create, and destroy instances, and react to the ready event through the JS API. Destroy returns the plain textarea field.

Usage Guidelines

Best Practices

  • Use the editor for user-authored rich content destined for NDS pages: announcements, article bodies, service descriptions, review notes
  • Use it for paste-heavy workflows where authors draft in Word or Google Docs: the paste pipeline converts their formatting instead of losing it or letting junk through
  • Do not use it for short single-line values or plain text. Use Text Fields instead
  • Do not use it as a code editor. The source view exists for occasional markup fixes, not as a primary authoring surface
  • Compose the toolbar to the use case with data-editor-toolbar: a comment field might ship bold italic | ul ol while a CMS field ships the full default set
  • The default heading commands are h2 h3 h4 because the page already owns its h1. Add the h1 token only for fields that author complete pages
  • Add required to the textarea (and data-required on the container) for mandatory fields: validation works natively because the value is a real form field
  • Links marked "Open in new tab" get target="_blank" with rel="noopener noreferrer" paired automatically, and only that target survives sanitization
  • Wire image uploads to your server with setImageUpload({ uploadUrl: '/api/…' }): files POST through the embedded NDS Upload (progress, retry, and error feedback included) and the returned URL is inserted. Without an endpoint the popover is URL-only, and pasted screenshots show a field notice instead of silently embedding
  • Reserve setImageUpload({ uploadUrl: 'embed' }) for fields without an upload backend: base64 bloats the stored value (~37% over the file size) and embedded images can't be cached or served optimized
  • To remove a pasted component, use the toolbar's remove button (it lists the component levels at the caret) or select the whole component and delete. Boundary deletes never break a component apart
  • Cap tall fields with --editor-max-size: past the cap the surface scrolls internally and the toolbar stays in reach
  • The whole component is beta: expect refinements while it hardens through real-project use

Keyboard Interactions

KeysAction
Ctrl/Cmd + B / I / UBold, italic, underline
EnterNew paragraph. Inside a pasted component: a line break instead, so the component structure never splits
Backspace / DeleteNormal editing in text and inside component parts. At a component boundary the delete stops instead of merging content through it
Tab / Shift+TabIn a list item: nest or un-nest the item. In source view: indent or outdent the line or selection
Escape, then TabIn source view: release the tab trap and move focus out of the field
Ctrl/Cmd + Z / YNative undo and redo

Toolbar Commands

Tokens for data-editor-toolbar. Space-separated, | starts a new button group, source renders at the bar's end, none opts out of the whole bar. Omit the attribute for the full default set.

TokenCommand
undo / redoNative history steps
bold / italic / underline / strikeInline formatting toggles
clearRemove inline formatting from the selection
linkLink popover: link text, URL, an "Open in new tab" option, and unlink for existing links
imageImage popover: insert from a link with alt text and width/height, edit a clicked image in place. An upload section appears when a server endpoint is configured or embedding is opted in
h2 / h3 / h4Heading toggles for the current block
h1Available but not in the default set: add it only for full-page authoring fields
align-start / align-center / align-end / align-justifyLogical block alignment that follows text direction
ul / olBulleted and numbered lists
removeDestructive component removal with a confirm popover listing the component levels at the caret
sourceHTML source view toggle, rendered at the end of the bar

Data Attributes

Configuration rides the native textarea attributes: placeholder, required, readonly, and disabled all carry over to the editing surface. The editor adds these attributes of its own:

AttributeDescription
data-editor-toolbarSet on the container to pick the toolbar commands. See the Toolbar Commands table for tokens and the grouping syntax
data-stateManaged on the container at runtime: readonly and disabled toggled through NDS.State. Initial state comes from the textarea's native attributes

CSS Custom Properties

PropertyDefaultDescription
--editor-min-size12remMinimum height of the editing surface and the source view
--editor-max-size70vhHeight cap: past it the surface scrolls internally so the toolbar stays visible

JavaScript API

The NDS.Editor API adopts textarea fields and creates and destroys instances. The submitted value is always the textarea, so reading and listening work like any form field.

// ── Initialization ─────────────────────────────────── // Auto-initializes every .nds-editor on page load, adopting the field's textarea. NDS.Editor.init(); // scan for new editors (also NDS.Editor.reinit()) const inst = NDS.Editor.create(el); // adopt one field; returns the instance, or null when no textarea is found NDS.Editor.destroy(el); // tear down: generated UI is removed, the plain textarea field remains el.ndsEditor; // instance backref on the root element // ── Ready event ────────────────────────────────────── el.addEventListener('nds:editor:ready', (e) => { e.detail.instance; // the editor instance }); // ── Value access ───────────────────────────────────── // The textarea IS the form field: sanitized, pretty-printed markup. const value = textarea.value; textarea.addEventListener('input', onEdit); // fires on every edit textarea.addEventListener('change', onCommit); // fires on blur when changed // ── Image upload configuration ─────────────────────── // The popover's upload section is a standard NDS Upload; setImageUpload // forwards ANY of its config keys to that container (they map 1:1 to the // data-* attributes on the File Upload page). Without setImageUpload the // popover is URL-only. Two modes for the uploadUrl: // - a real endpoint: files POST one per request ("file" field), and the // response's { url } (or a bare URL body) is inserted — no base64. // - 'embed' (reserved sentinel): files embed as data:image URLs. No POST // is sent; for demos and back-office fields without an upload backend. el.addEventListener('nds:editor:ready', (e) => { e.detail.instance.setImageUpload({ uploadUrl: '/api/images', // server endpoint, or 'embed' for base64 (data-upload-url) autoUpload: true, // upload on selection; ignored for 'embed' (data-auto-upload) maxFileSize: 5 * 1024 * 1024, // bytes; generated default: 2MB (data-max-file-size) allowedTypes: 'jpg,png,webp', // generated default: jpg,jpeg,png,gif,webp,svg (data-allowed-types) allowedMimeTypes: 'image/*', // optional second check (data-allowed-mime-types) }); }); // The same 2MB default cap also gates screenshot paste in embed mode — // raising maxFileSize raises both. Size/type violations render in the file // chip; paste violations use the field's feedback message. // NDS Upload events (nds:upload:success, nds:upload:error…) bubble from the // container for custom handling — see the File Upload component page. // ── Runtime states ─────────────────────────────────── // Initial state comes from the textarea's readonly / disabled attributes. NDS.State.add(el, 'readonly'); // or 'disabled' NDS.State.remove(el, 'readonly');
Last Modified Date: 19/07/2026 - 04:00 AM
Was this page useful?
60% of users said Yes from 2843 Feedbacks