Senren UI
Components / Rich Text Editor Lite

Rich Text Editor Lite

Stable Stimulus: senren--rich-text-editor-lite

light formatting (bold/italic/links/lists) only

Release notes

Write short formatted content without adding a heavy editor dependency.

Usage example #

Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.

app/views/.../rich_text_editor_lite_example.html.erb
<div class="w-full max-w-lg">
  <%= render Senren::RichTextEditorLiteComponent.new(
    name: "release_notes",
    label: "Release notes",
    value: "<p><strong>Release notes</strong></p><p>Write short formatted content without adding a heavy editor dependency.</p>"
  ) %>
</div>

Install this component #

Copy the official component into your app

This component requires Stimulus. Keep --client so the controller is copied with the ViewComponent.

Terminal
bin/rails senren:add rich_text_editor_lite --client

Create a custom component with the same conventions

Use this when you need an app-specific component that follows Senren's ViewComponent and Stimulus structure. --client is required for this behavior.

Terminal
bin/rails generate senren:component rich_text_editor_lite --client

Dependencies are resolved by senren:add: button.

At a glance #

Category Rich
Class name Senren::RichTextEditorLiteComponent
Stimulus senren--rich-text-editor-lite
Variants default
Depends on button
Pairs with form, label

Source #

app/components/senren/rich_text_editor_lite_component.html.erb
<%= tag.div(**root_attrs("overflow-hidden rounded-(--senren-radius) border shadow-sm", data: { controller: "senren--rich-text-editor-lite", "senren--rich-text-editor-lite-debug-value": debug? })) do %>
  <label for="<%= dom_id %>-editor" class="sr-only"><%= label %></label>
  <textarea hidden name="<%= name %>" data-senren--rich-text-editor-lite-target="input"><%= initial_content %></textarea>

  <% if toolbar? %>
    <div class="flex flex-wrap items-center gap-1 border-b border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-muted)/0.35)] p-2">
      <% [
        ["formatBlock:p", "P", "Paragraph"],
        ["formatBlock:h1", "H1", "Heading 1"],
        ["formatBlock:h2", "H2", "Heading 2"],
        ["formatBlock:h3", "H3", "Heading 3"],
        ["bold", "B", "Bold"],
        ["italic", "I", "Italic"],
        ["createLink", "Link", "Create link"],
        ["insertUnorderedList", "List", "Bulleted list"],
        ["insertOrderedList", "1.", "Numbered list"],
        ["align:left", "Left", "Align left"],
        ["align:center", "Center", "Align center"],
        ["align:right", "Right", "Align right"],
        ["align:justify", "Justify", "Justify"]
      ].each do |command, label_text, aria_label| %>
        <button type="button" disabled class="inline-flex h-8 cursor-pointer items-center rounded-(--senren-radius) px-2 text-sm font-medium text-[hsl(var(--senren-foreground))] hover:bg-[hsl(var(--senren-accent))] disabled:cursor-not-allowed disabled:opacity-50 aria-pressed:bg-[hsl(var(--senren-accent))]" data-senren--rich-text-editor-lite-target="button" data-command="<%= command %>" data-action="pointerdown->senren--rich-text-editor-lite#keepSelection mousedown->senren--rich-text-editor-lite#keepSelection touchstart->senren--rich-text-editor-lite#keepSelection click->senren--rich-text-editor-lite#format" aria-label="<%= aria_label %>" aria-pressed="false">
          <%= label_text %>
        </button>
      <% end %>
    </div>
  <% end %>

  <div id="<%= dom_id %>-editor" role="textbox" aria-multiline="true" aria-label="<%= label %>" contenteditable="true" data-placeholder="<%= placeholder %>" data-senren--rich-text-editor-lite-target="editor" data-action="click->senren--rich-text-editor-lite#openLink input->senren--rich-text-editor-lite#sync input->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#rememberSelection keyup->senren--rich-text-editor-lite#updateToolbar mouseup->senren--rich-text-editor-lite#rememberSelection mouseup->senren--rich-text-editor-lite#updateToolbar touchend->senren--rich-text-editor-lite#rememberSelection paste->senren--rich-text-editor-lite#syncSoon" class="min-h-40 cursor-text bg-[hsl(var(--senren-background))] p-4 text-left text-sm leading-6 text-[hsl(var(--senren-foreground))] outline-none focus:ring-2 focus:ring-inset focus:ring-[hsl(var(--senren-ring))]">
    <%= sanitize(initial_content, tags: %w[p br strong b em i a ul ol li h1 h2 h3], attributes: %w[href rel target data-align]) %>
  </div>
<% end %>
app/components/senren/rich_text_editor_lite_component.rb
module Senren
  class RichTextEditorLiteComponent < BaseComponent
    VARIANTS = {
      default: 'border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-card))]'
    }.freeze
    SIZES = { md: '' }.freeze

    def initialize(name: 'content', value: nil, label: 'Content', placeholder: 'Write something...', id: nil,
                   toolbar: true, debug: ::Rails.env.development?, class_name: nil, **html)
      super(variant: :default, size: :md, class_name: class_name, **html)
      @name = name
      @value = value
      @label = label
      @placeholder = placeholder
      @dom_id = id || "senren-editor-#{SecureRandom.hex(3)}"
      @toolbar = toolbar
      @debug = debug
    end

    attr_reader :name, :value, :label, :placeholder, :dom_id

    def toolbar? = !!@toolbar

    def debug? = !!@debug

    def initial_content
      value.presence || content.to_s.presence || "<p>#{ERB::Util.html_escape(placeholder)}</p>"
    end
  end
end

AI agent rules #

Use for

  • +light formatting (bold/italic/links/lists) only

Avoid

  • -tables
  • -embeds
  • -media - out of scope for v0.1

Accessibility #

  • Toolbar buttons must have aria-label; use contenteditable role=textbox.