Trix é um editor de texto WYSIWYG (what you see is what you get, ou “o que você vê é o que você recebe”) desenvolvido pela 37signals para o ecossistema do Ruby on Rails. Seu objetivo é oferecer uma solução simples e eficiente para os casos de uso mais comuns de edição de texto, sem deixar de lado a possibilidade de customizações avançadas.
Criado originalmente para o Basecamp — o conhecido sistema de gerenciamento de projetos da 37signals — o Trix se destaca por sua leveza, integração direta com Rails e uma arquitetura que permite extensões e personalizações. Essa característica se deve em parte à filosofia dos desenvolvedores da empresa, que evitam adicionar funcionalidades consideradas desnecessárias ao seu próprio produto. Felizmente, essa mesma simplicidade abre espaço para que cada projeto adapte o editor às suas próprias necessidades.
Integração com Stimulus, HTML e Simple Form
Na Acsiv, utilizamos extensivamente o Stimulus, e por isso toda a customização do Trix também passa por ele. A inicialização mais simples do editor fica assim:
<div data-controller="trix">
<trix-editor></trix-editor>
</div>
Como também utilizamos a gem Simple Form, e o Trix aparece em praticamente todos os campos de rich text, criei um input customizado para simplificar o uso e evitar repetição de configuração em cada formulário:
<%= f.input :draft, as: :rich_text_area %>
Criando um input personalizado no Simple Form
Se você não usa Simple Form ou já está familiarizado com a criação de inputs customizados, pode pular esta parte.
No Simple Form, um input personalizado nada mais é do que uma classe que define como aquele tipo de campo deve ser renderizado. Para que possamos escrever as: :rich_text_area nos formulários, precisamos criar um RichTextAreaInput seguindo a convenção da gem:
class RichTextAreaInput < SimpleForm::Inputs::Base
def input(wrapper_options = nil)
merged_options = merge_wrapper_options(input_html_options, wrapper_options)
@builder.template.content_tag(:div, data: { controller: "trix" }) do
@builder.rich_text_area(attribute_name, merged_options)
end
end
end
Customizando a barra de ferramentas
Para a maioria dos usuários que desejam modificar o Trix, começar pela barra de ferramentas é o passo mais natural. Uma abordagem bastante comum é manipular diretamente o DOM após o editor ser carregado — buscando elementos, removendo botões ou adicionando outros. Embora funcione, esse caminho tende a gerar código mais frágil e difícil de manter.
A forma que considero mais interessante é sobrescrever a função responsável por gerar o HTML da barra de ferramentas: Trix.config.toolbar.getDefaultHTML(). Essa função retorna exatamente a estrutura HTML utilizada pelo Trix, e substituí-la dá total liberdade para definir sua própria toolbar.
No nosso caso, mantive o HTML da barra separado do controller Stimulus para melhor organização do código:
// trix_controller.js
import { Controller } from "@hotwired/stimulus"
import { trixToolbarHTML } from "../services/trix_toolbar"
export default class extends Controller {
connect() {
Trix.config.toolbar.getDefaultHTML = this.toolbarDefaultHTML
}
toolbarDefaultHTML() {
return trixToolbarHTML
}
}
E aqui está o arquivo de toolbar customizada:
// trix_toolbar.js
export const trixToolbarHTML = `
<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" tabindex="-1">Bold</button>
</span>
</div>
`
O código acima deixa apenas o botão de negrito na barra de ferramentas, mas já demonstra como a customização é simples. A partir daqui, você pode alterar completamente a aparência (no meu caso, utilizei Bootstrap) e estruturar a barra de ferramentas da forma que fizer mais sentido para o seu projeto.
Se quiser entender melhor tudo o que o Trix oferece por padrão, recomendo conferir a barra de ferramentas original. Ela serve como um ótimo ponto de referência para conhecer todos os grupos e botões disponíveis.
E é justamente ao analisar o código padrão que surge a pergunta: como criar novos recursos?
Afinal, o Trix entrega apenas negrito, itálico e tachado.
Mas e se eu quiser ir além e adicionar sublinhado, sobrescrito, subscrito, marcador de texto e outros comandos totalmente customizados?
Adicionando novos recursos ao Trix
Vamos começar adicionando botões na barra de ferramentas (vou omitir as classes CSS para focar no essencial):
// trix_toolbar.js
export const trixToolbarHTML = `
<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" data-trix-attribute="bold" data-trix-key="b" tabindex="-1">Bold</button>
<button type="button" data-trix-attribute="italic" data-trix-key="i" tabindex="-1">Italic</button>
<button type="button" data-trix-attribute="underline" data-trix-key="u" tabindex="-1">Underline</button>
<button type="button" data-trix-attribute="strike" tabindex="-1">Strike</button>
<button type="button" data-trix-attribute="superscript" data-trix-key="." tabindex="-1">Superscript</button>
<button type="button" data-trix-attribute="subscript" data-trix-key="," tabindex="-1">Subscript</button>
<button type="button" data-trix-attribute="highlight" data-trix-key="m" tabindex="-1">Highlighter</button>
</span>
</div>
`
Note que todos os botões possuem um atributo data-trix-attribute. Ao ser clicado, o Trix procura nas configurações internas (Trix.config.textAttributes) um atributo com o mesmo nome e aplica seu efeito ao texto selecionado.
Além disso, também podemos definir um atributo data-trix-key que configura a tecla de atalho que aplica esse recurso ao ser usada em conjunto com a tecla meta.
Para manter a estrutura organizada, podemos definir esses atributos em um arquivo dedicado, carregado globalmente pela aplicação (por exemplo em application.js, se você estiver usando esbuild):
// trix_custom_attributes.js
Trix.config.textAttributes.underline = {
tagName: "u",
inheritable: true
}
Trix.config.textAttributes.subscript = {
tagName: "sub",
inheritable: true,
}
Trix.config.textAttributes.superscript = {
tagName: "sup",
inheritable: true,
}
Trix.config.textAttributes.highlight = {
tagName: "mark",
inheritable: true,
}
Sempre que o Trix encontrar um atributo correspondente ao botão clicado, ele envolverá o texto selecionado no elemento especificado em tagName. Como todos os exemplos acima usam tags HTML5 nativas, o comportamento é imediato e intuitivo.
O objeto de configuração de um atributo de texto permite diversas opções:
| Atributo | Tipo | Descrição |
|---|---|---|
tagName | String | Define qual tag HTML envolverá o texto ("strong", "em", "u", "mark"…). |
className | String | (Opcional) Classe CSS aplicada à tag gerada. |
style | Object | (Opcional) Estilos inline aplicados à tag. Ex: { color: "red" }. |
inheritable | Boolean | Se true, novos caracteres digitados herdam automaticamente esse atributo enquanto ativo. |
parser | Function | (Opcional) Função que diz ao Trix como detectar automaticamente esse atributo ao colar HTML. |
Trix também possui atributos de bloco, que, em vez de serem aplicados a caracteres individuais, sempre afetam o bloco inteiro de conteúdo. Esse é o caso de componentes como citações, títulos e listas, por exemplo. A sintaxe para definir um atributo de bloco é simples:
Trix.config.blockAttributes.quote = {
tagName: "blockquote",
nestable: true,
},
O objeto de configuração dos atributos de bloco também permitem diversas opções:
| Atributo | Tipo | Descrição |
|---|---|---|
tagName | String | Define qual tag HTML envolverá o texto ("blockquote", "h1", "ul"…). |
exclusive | Boolean | Indica que esse atributo é exclusivo: ao ser aplicado, remove qualquer outro atributo de bloco ativo no mesmo bloco. |
nestable | Boolean | Permite que esse bloco seja aninhado dentro de outro bloco (ex.: blockquote dentro de blockquote). |
terminal | Boolean | Indica que o bloco é terminal. Blocos terminais não podem conter outros blocos dentro deles. |
parse | Boolean | Controla se o Trix deve reconhecer automaticamente esse bloco ao importar ou colar HTML correspondente. |
group | Boolean | Agrupa atributos de bloco relacionados, permitindo que o Trix trate esses atributos como mutuamente exclusivos dentro do mesmo grupo. |
breakOnReturn | Boolean | Quando true, pressionar Enter encerra o bloco atual e cria um novo bloco padrão (ex: sair de um título ao pressionar Enter). |
Adicionando recursos personalizados
Como você já deve ter percebido, até aqui utilizamos tags HTML5 nativas nos atributos criados. No entanto, o Trix permite ir além: ele cria um elemento com o valor definido em tagName independentemente de a tag existir oficialmente no HTML.
Aproveitando essa flexibilidade, vamos criar quatro botões para controlar o alinhamento do bloco: alinhado à esquerda, centralizado, alinhado à direita e justificado.
// trix_toolbar.js
export const trixToolbarHTML = `
<div class="trix-button-row">
<span class="trix-button-group trix-button-group--alignment-tools" data-trix-button-group="alignment-tools">
<button type="button" data-trix-attribute="alignLeft" tabindex="-1">left</button>
<button type="button" data-trix-attribute="alignCenter" tabindex="-1">center</button>
<button type="button" data-trix-attribute="alignRight" tabindex="-1">right</button>
<button type="button" data-trix-attribute="justify" tabindex="-1">justify</button>
</span>
</div>
`
Agora definimos os atributos de bloco correspondentes. Note que estamos utilizando tags customizadas, que não possuem significado semântico por padrão:
// trix_custom_attributes.js
Trix.config.blockAttributes.alignLeft = {
tagName: "align-left",
}
Trix.config.blockAttributes.alignCenter = {
tagName: "align-center",
}
Trix.config.blockAttributes.alignRight = {
tagName: "align-right",
}
Trix.config.blockAttributes.justify = {
tagName: "justify",
}
Ao clicar em qualquer um desses botões, o Trix aplicará a tag customizada ao bloco selecionado. No entanto, visualmente nada acontecerá — afinal, essas tags não possuem comportamento nativo.
Para dar significado a esses elementos, precisamos definir seu estilo. Embora seja possível criar componentes JavaScript dedicados para cada caso, uma solução simples e eficaz é aplicar regras CSS diretamente:
/* trix.css */
align-left { text-align: left; width: 100%; display: block; }
align-center { text-align: center; width: 100%; display: block; }
align-right { text-align: right; width: 100%; display: block; }
justify { text-align: justify; width: 100%; display: block; }
Com isso, ao clicar em qualquer um dos novos botões, o bloco será alinhado conforme o comando escolhido.
Adicionando recursos com lógica JavaScript
Até aqui, todos os recursos adicionados são relativamente simples e não exigem lógica adicional: a própria API do Trix já se encarrega de selecionar atributos e envolver trechos de texto com elementos previamente definidos.
A seguir, vamos implementar um seletor de tamanho de fonte (e, na sequência, um seletor de cores). Em ambos os casos, precisamos garantir que apenas uma opção esteja ativa por vez, sem exceções. Esse comportamento não é oferecido diretamente pelas opções de configuração dos atributos (ao menos, não encontrei suporte nativo para isso), o que nos obriga a introduzir um pouco de lógica JavaScript.
Felizmente, o Trix facilita bastante esse processo ao permitir a definição de ações customizadas, disparadas sempre que um botão da toolbar é clicado. Vamos começar implementando o seletor de tamanhos seguindo a mesma abordagem utilizada nos exemplos anteriores.
// trix_toolbar.js
export const trixToolbarHTML = `
<div class="trix-button-row">
<span class="trix-button-group trix-button-group--font-size-tools" data-trix-button-group="font-size-tools">
<button type="button" data-trix-attribute="textSize1" data-trix-action="x-exclusive-text-size" tabindex="-1">
textSize1
</button>
<button type="button" data-trix-attribute="textSize2" data-trix-action="x-exclusive-text-size" tabindex="-1">
textSize2
</button>
<button type="button" data-trix-attribute="textSize3" data-trix-action="x-exclusive-text-size" tabindex="-1">
textSize3
</button>
<button type="button" data-trix-attribute="textSize4" data-trix-action="x-exclusive-text-size" tabindex="-1">
textSize4
</button>
<button type="button" data-trix-attribute="textSize5" data-trix-action="x-exclusive-text-size" tabindex="-1">
textSize5
</button>
</span>
</div>
`
O ponto mais importante no trecho acima é o atributo data-trix-action. É nele que definimos o identificador do evento que será disparado ao clicar no botão. O Trix exige que essas ações customizadas comecem com o prefixo x-, garantindo que não entrem em conflito com ações internas do editor.
Agora, definimos os atributos de texto correspondentes:
// trix_custom_attributes.js
for (let i = 1; i <= 5; i++) {
Trix.config.textAttributes[`textSize${i}`] = {
tagName: `text-size-${i}`,
inheritable: true,
}
}
E, por fim, o CSS responsável por aplicar o efeito visual:
/* trix.css */
text-size-1 { font-size: 20px; }
text-size-2 { font-size: 24px; }
text-size-3 { font-size: 28px; }
text-size-4 { font-size: 32px; }
text-size-5 { font-size: 40px; }
Com isso, ao clicar em qualquer um dos botões, o tamanho da fonte do trecho selecionado será alterado conforme o valor desejado. No entanto, ainda existe um problema: é possível ativar todos os botões ao mesmo tempo, fazendo com que o texto seja envolvido por múltiplos elementos — e apenas o último aplicado terá efeito visual.
Para evitar esse comportamento, precisamos garantir que, ao selecionar um tamanho, todos os outros sejam automaticamente desativados. É aqui que entra a lógica JavaScript:
// trix_controller.js
import { Controller } from "@hotwired/stimulus"
import { trixToolbarHTML } from "../services/trix_toolbar"
export default class extends Controller {
connect() {
Trix.config.toolbar.getDefaultHTML = this.toolbarDefaultHTML
}
toolbarDefaultHTML() {
return trixToolbarHTML
}
installEventListeners() {
addEventListener("trix-action-invoke", this.applyExclusiveTextSize)
}
applyExclusiveTextSize = ({ target, invokingElement, actionName }) => {
if (actionName === "x-exclusive-text-size") {
const attribute = invokingElement.dataset["trixAttribute"]
for (let i = 1; i <= 5; i++) {
let runningAttribute = `textSize${i}`
if (attribute === runningAttribute) continue
target.editor.deactivateAttribute(runningAttribute)
}
}
}
}
Com essa abordagem, sempre que um tamanho de fonte for selecionado, todos os outros atributos de tamanho serão desativados automaticamente, garantindo que apenas um esteja ativo por vez — exatamente o comportamento esperado para esse tipo de controle.
Para o seletor de cores, o processo é essencialmente o mesmo. A única diferença significativa está nos efeitos visuais aplicados ao texto:
// trix_toolbar.js
export const trixToolbarHTML = `
<div class="trix-button-row">
<span class="trix-button-group trix-button-group--font-color-tools" data-trix-button-group="font-color-tools">
<button type="button" data-trix-attribute="redText" data-trix-action="x-exclusive-color" tabindex="-1">Red</button>
<button type="button" data-trix-attribute="greenText" data-trix-action="x-exclusive-color" tabindex="-1">Green</button>
<button type="button" data-trix-attribute="blueText" data-trix-action="x-exclusive-color" tabindex="-1">Blue</button>
</span>
</div>
`
Definimos então os atributos de texto correspondentes:
// trix_custom_attributes.js
["red", "green", "blue"].forEach((color) => {
Trix.config.textAttributes[`${color}Text`] = {
tagName: `${color}-text`,
inheritable: true
}
})
E os estilos responsáveis pelo efeito visual:
/* trix.css */
red-text { color: #dc3545; }
green-text { color: #198754; }
blue-text { color: #0d6efd; }
Por fim, adicionamos a lógica responsável por garantir que apenas uma cor esteja ativa por vez, reaproveitando o mesmo mecanismo de eventos customizados:
// trix_controller.js
import { Controller } from "@hotwired/stimulus"
import { trixToolbarHTML } from "../services/trix_toolbar"
export default class extends Controller {
connect() {
Trix.config.toolbar.getDefaultHTML = this.toolbarDefaultHTML
}
toolbarDefaultHTML() {
return trixToolbarHTML
}
installEventListeners() {
addEventListener("trix-action-invoke", this.applyExclusiveTextSize)
addEventListener("trix-action-invoke", this.applyExclusiveColor)
}
applyExclusiveTextSize = ({ target, invokingElement, actionName }) => {
if (actionName === "x-exclusive-text-size") {
const attribute = invokingElement.dataset["trixAttribute"]
for (let i = 1; i <= 5; i++) {
let runningAttribute = `textSize${i}`
if (attribute === runningAttribute) continue
target.editor.deactivateAttribute(runningAttribute)
}
}
}
applyExclusiveColor = ({ target, invokingElement, actionName }) => {
if (actionName === "x-exclusive-color") {
const attribute = invokingElement.dataset["trixAttribute"]
const colors = ["red", "green", "blue"]
colors.forEach((color) => {
let runningAttribute = `${color}Text`
if (attribute === runningAttribute) return
target.editor.deactivateAttribute(runningAttribute)
})
}
}
}
Na Acsiv, como já mencionado, utilizamos Bootstrap para estilizar o produto — e com o Trix não foi diferente. Nossa paleta de cores faz sentido para o nosso contexto, mas pode não se adequar ao seu caso de uso. Por isso, os exemplos acima foram mantidos propositalmente simples. Ainda assim, nada impede que você implemente elementos visuais mais sofisticados, como dropdowns, e selecione as cores a partir deles.
você tem total liberdade para criar componentes com lógica extra, adaptados exatamente ao seu caso de uso. A API do Trix não impõe grandes limitações nesse sentido — pelo contrário, ela incentiva esse tipo de extensão.
Um bom exemplo disso é um botão responsável por remover todos os atributos atualmente aplicados a um trecho de texto — para você não precisar desativar eles um por um 😊:
resetFormattingAttributes = ({ target, invokingElement, actionName }) => {
if (actionName === "x-reset-formatting-attributes") {
Object.keys(Trix.config.textAttributes).forEach(attribute => {
if (target.editor.attributeIsActive(attribute)) target.editor.deactivateAttribute(attribute)
})
Object.keys(Trix.config.blockAttributes).forEach(attribute => {
if (target.editor.attributeIsActive(attribute)) target.editor.deactivateAttribute(attribute)
})
}
}
Nesse caso, percorremos todos os atributos de texto e atributos de bloco registrados no Trix e desativamos apenas aqueles que estão ativos no editor no momento da ação. O resultado é um comportamento semelhante ao botão “limpar formatação” encontrado em editores mais completos — mas totalmente customizável.
Esse padrão reforça um dos pontos mais interessantes do Trix: embora ele ofereça uma API simples, ela é poderosa o suficiente para permitir a construção de comportamentos avançados sem a necessidade de hacks ou dependências externas.
Finalizando
Com tudo o que vimos até aqui, você já deve ser capaz de implementar a maioria das customizações que provavelmente irá precisar no Trix. Ainda assim, vale destacar dois pontos importantes que costumam passar despercebidos em um primeiro momento, mas que são essenciais em projetos reais: sanitização de conteúdo e traduções.
Sanitizações
Ao utilizar tags HTML customizadas, é fundamental garantir que elas não sejam removidas durante o processo de sanitização do conteúdo. Isso vale tanto para o Trix, no lado do cliente, quanto para o ActiveRecord, no backend — especialmente se você utiliza has_rich_text.
No caso do Trix, o processo é simples. O editor já utiliza a biblioteca DOMPurify, então basta informar explicitamente quais tags adicionais devem ser permitidas:
Trix.config.dompurify.ADD_TAGS = ["text-size-1", "text-size-2", "text-size-3", "text-size-4", "text-size-5", "red-text", "green-text", "blue-text" "align-left", "align-center", "align-right", "justify"]
No backend, o processo é igualmente direto. Caso você utilize Action Text, é necessário estender a whitelist de tags permitidas pelo sanitizador do Rails. Um initializer simples já resolve o problema:
Rails.application.config.after_initialize do
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "text-size-1"
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "text-size-2"
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "text-size-3"
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "text-size-4"
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "text-size-5"
%w[red green blue].each do |color|
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "#{color}-#text"
end
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "align-left"
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "align-center"
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "align-right"
Rails::Html::Sanitizer.white_list_sanitizer.allowed_tags << "justify"
end
Com isso, suas tags customizadas passam a ser preservadas tanto no editor quanto na persistência dos dados.
Traduções
As traduções seguem a mesma filosofia do restante da API do Trix: são simples, diretas e fáceis de estender.
Primeiro, defina os textos desejados no objeto de configuração de idioma:
Trix.config.lang.textSize1 = "Tamanho 1"
Trix.config.lang.textSize2 = "Tamanho 2"
Trix.config.lang.textSize3 = "Tamanho 3"
Trix.config.lang.textSize4 = "Tamanho 4"
Trix.config.lang.textSize5 = "Tamanho 5"
Em seguida, utilize essas chaves diretamente no HTML da toolbar:
// trix_toolbar.js
const { lang } = Trix.config
export const trixToolbarHTML = `
<div class="trix-button-row">
<span class="trix-button-group trix-button-group--font-size-tools" data-trix-button-group="font-size-tools">
<button type="button" data-trix-attribute="textSize1" data-trix-action="x-exclusive-text-size" tabindex="-1">
${lang.textSize1}
</button>
<button type="button" data-trix-attribute="textSize2" data-trix-action="x-exclusive-text-size" tabindex="-1">
${lang.textSize2}
</button>
<button type="button" data-trix-attribute="textSize3" data-trix-action="x-exclusive-text-size" tabindex="-1">
${lang.textSize3}
</button>
<button type="button" data-trix-attribute="textSize4" data-trix-action="x-exclusive-text-size" tabindex="-1">
${lang.textSize4}
</button>
<button type="button" data-trix-attribute="textSize5" data-trix-action="x-exclusive-text-size" tabindex="-1">
${lang.textSize5}
</button>
</span>
</div>
`
Esse padrão facilita não só a tradução do editor, mas também sua futura manutenção e expansão para múltiplos idiomas.
Encerrando de vez
Com esses ajustes, é bem provável que você consiga adaptar o Trix completamente ao seu contexto de uso — desde pequenas customizações visuais até comportamentos mais complexos controlados por JavaScript.
Existe, no entanto, uma funcionalidade bastante requisitada que o Trix não oferece nativamente: suporte a tabelas HTML. Essa foi uma decisão consciente dos desenvolvedores originais, mas que nem sempre atende às necessidades de produtos mais complexos.
Na Acsiv, por exemplo, o uso de tabelas era indispensável. Por isso, implementamos esse suporte por conta própria. No próximo artigo, vou mostrar em detalhes como construímos suporte a tabelas no Trix, respeitando sua arquitetura e mantendo uma boa experiência de edição.
Se você chegou até aqui, já tem base mais do que suficiente para ir além do básico — e, com um pouco de paciência, transformar o Trix em um editor muito mais poderoso do que ele aparenta à primeira vista.
Escrito por: Fillipe Palhares