コンテンツへスキップ

スキン ​

スキンは、エディターの外観(クローム)の 1 つのスタイルです。ツールバー、その中のボタンとグループ、バブルメニュー、ドロップダウンメニュー、ダイアログ、コンテンツを囲むフレーム、画像の選択時の装飾が対象です。skin prop で選択します。

vue
<Tiptapify skin="pastel" />

スキンは トークン(CSS 変数としての色とサイズ)と シェイプ(トークンでは表現できない構造上のバリアント)の 2 つから成ります。組み込みスキンは default(クラシックな外観で、浮き上がったボタングループを持つ浮き上がったツールバー)と、落ち着いた外観の pastel の 2 つです。カスタムスキンもファーストクラスです。registerSkin() とインラインのスキンオブジェクトはまったく同じ仕組みで解決され、組み込みスキンも特別扱いではなく登録済みスキンです。

用語: スキンと Vuetify テーマ ​

  • skin(スキン) — エディターの外観の 1 つのスタイルを指す、ユーザー向けの用語です。トークンとシェイプで構成されます。prop は skin、型は TiptapifySkin*、レジストリは registerSkin()、ルートのクラスは tiptapify-skin-{name} です。
  • theme(テーマ) — Vuetify のライト/ダークテーマ専用の用語です。Tiptapify のコンポーネントは theme prop に Vuetify の現在のテーマ名を渡し続け、スキンの仕組みがそれに触れることはありません。

両者は組み合わさって動作します。組み込みのトークンの色の多くは Vuetify のテーマ変数(rgb(var(--v-theme-surface)) など)を参照しているため、スキンはライト/ダークに自動で追従します。クラシック列で固定のダーク色になっているもの、つまりリサイズハンドル(rgba(0, 0, 0, .5/.8))、通常の画像アウトライン、バブルの影、メニューとダイアログのエレベーションの影は意図的なものです。クラシックの外観はどちらのテーマでも中立的なダーク色のクロームを描画し、計測済みのベースラインがそれを固定しています。カスタムスキンでは、テーマ変数を自由に参照できます。トークン表をご覧ください。

コンセプト ​

トークン ​

トークンは --tf-* という名前の CSS カスタムプロパティです。カタログのキーは、CSS サフィックスをキャメルケースにしたものです。toolbarBg → --tf-toolbar-bg、dialogTitleSize → --tf-dialog-title-size。値はプレーンな文字列で、色、完全な border の省略記法、影、パディング、角丸などです。

ライブラリ内のスキンルールはすべてただ 1 か所に存在し、クラシック(ベース)の値と同じフォールバックを付けてトークンを読み取ります。

css
.tiptapify-toolbar-group {
  border-radius: var(--tf-group-radius, 4px);
}

解決されたスキンは、上書き分だけを変数として出力し、ベースの値は CSS のフォールバックが担います。変数はエディターのルートに設定されます。また、メニューとダイアログはルートの外にテレポートされるため、各メニューの SkinScope ラッパーとダイアログカード自体にも設定され、トークンはあらゆる面に追従します。

シェイプ ​

6 つのフラグが、どのルールセットを適用するかを決め、トークンがそれらを描画します。

フラグ値切り替える内容
toolbarraised · flatVToolbar のエレベーションと内側の罫線なし、またはフラットな色付きバー
groupsbox · flatツールバーのセクションを浮き出した VBtnGroup ボックス(0.3.2 のマークアップ: ボタンが連結され、グループのトークンがボックスを描画)にするか、通常の flex の div の並びにするか
buttonsvuetify · quietネイティブの Vuetify ボタン、または押下時にニュートラルな状態になるゴーストボタン
bubblecard · flat互換性のために残されています。バブルメニューは、どちらのシェイプでも、トークンで描画されたアイコンボタンの 1 行です(ボタンは buttons に従います)
dialogclassic · softタイトル下の区切り線と浮き出した閉じるボタン、またはやわらかいカードとテキストの閉じるボタン
menuvuetify · softドロップダウンメニュー: Vuetify のリスト、または余白のある明るいカード

解決されたシェイプは、エディターのルートに 6 つの .tiptapify-shape-{flag}-{value} クラスとして付与され、カスタムツールバーコンポーネント向けに useTiptapifyConfig() の shape: ComputedRef<ResolvedShape> として公開されます。

拡張(Extends) ​

スキンは、クラシックのベースの上に、extends チェーンをルート側から順に、最後に自身のメンバーを重ねて解決されます。

classic (base tokens + classic shape) → extends chain → own colors/metrics/flags

存在しないリンクは、そこまでに蓄積されたチェーンに解決され(開発時には警告が出ます)、循環はクラシックに解決されます(開発時には警告が出ます)。登録済みのスキンは名前で参照します。インラインのスキンオブジェクトは登録済みの名前を extends できますが、グローバルレジストリには登録されないため、カスタムスキンがエディター間で漏れることはありません。未知のトークンキーは、開発時の警告とともに無視されます。

3 つの形式すべてのライブデモは、スキンの例のページにあります。

トークンカタログ ​

以下の表は v1 の仕様です。カタログのキー(TiptapifySkinColors / TiptapifySkinMetrics)が、設定できる名前になります。クラシック列の値は CSS のフォールバック、つまりスキンがトークンを上書きしていないときにベースのルールが描画する値です。

ツールバー / バー ​

トークンCSS 変数クラシックpastel
toolbarBg--tf-toolbar-bgrgb(var(--v-theme-surface))rgba(var(--v-theme-on-surface), 0.03)
toolbarBorderBottom--tf-toolbar-border-bottomnone1px solid rgba(var(--v-border-color), var(--v-border-opacity))
toolbarFg--tf-toolbar-fginheritrgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))

項目 / グループ / 区切り ​

トークンCSS 変数クラシックpastel
itemsPadding--tf-items-padding12px 16px6px 8px
itemsGap--tf-items-gapnormal ¹6px
itemsRowGap--tf-items-row-gap5px4px
groupGap--tf-group-gap0px2px
groupHeight--tf-group-height32pxauto
groupBg--tf-group-bgrgb(var(--v-theme-surface))transparent
groupRadius--tf-group-radius4px0px
groupShadow--tf-group-shadowVuetify エレベーション 2 ²none
dividerWidth--tf-divider-width0px (スペーサー: margin 0 4px)1px
dividerHeight--tf-divider-height0px20px
dividerColor--tf-divider-colortransparentrgba(var(--v-border-color), var(--v-border-opacity))

¹ 計測されたクラシックの値は normal で、これは Vuetify ネイティブの flex の値であり、CSS のフォールバックが持っているものです。カタログのベース値 0px は、スキンで上書きするときに使える同等の明示的な長さです。

² 「Vuetify elevation n」と書かれたセルは説明用です。正確な 3 重の影の文字列は、コミット済みのベースラインフィクスチャ(src/skins/__fixtures__/skin-baseline.json)にある計測値で、CSS のフォールバックでもあります。groupShadow では elevation 2(0 3px 1px -2px …, 0 2px 2px 0 …, 0 1px 5px 0 …)がそれに当たります。groups: 'box' ではセクションは VBtnGroup(elevation="4")で、トークンのルールがグループ自身のエレベーションの上にこの 3 重の影でボックスを描画します。エレベーションを持つのはボックスで、連結された子ボタンは持ちません。

ボタン ​

これらのトークンは buttons: 'quiet' のときだけ使われます。到達可能性を参照してください。

トークンCSS 変数quiet の値(クラシック: ネイティブの Vuetify)
buttonFg--tf-button-fgrgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))
buttonHoverBg--tf-button-hover-bgrgba(var(--v-theme-on-surface), 0.07)
buttonHoverFg--tf-button-hover-fgrgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity))
buttonActiveBg--tf-button-active-bgrgba(var(--v-theme-on-surface), 0.1)
buttonActiveHoverBg--tf-button-active-hover-bgrgba(var(--v-theme-on-surface), 0.14)
buttonRadius--tf-button-radius6px

バブルメニュー(選択、空行、画像) ​

トークンCSS 変数クラシックpastel
bubbleBg--tf-bubble-bgrgb(var(--v-theme-surface))同じ
bubbleBorder--tf-bubble-bordernone1px solid rgba(var(--v-border-color), var(--v-border-opacity))
bubbleShadow--tf-bubble-shadow0 4px 14px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.08)0 6px 20px rgba(0, 0, 0, 0.12)
bubbleRadius--tf-bubble-radius6px10px
bubblePadding--tf-bubble-padding4px同じ
bubbleGap--tf-bubble-gap2px同じ
トークンCSS 変数クラシックpastel
menuListPadding--tf-menu-list-padding8px 0px4px
menuRadius--tf-menu-radius4px8px
menuBorder--tf-menu-bordernone1px solid rgba(var(--v-border-color), var(--v-border-opacity))
menuShadow--tf-menu-shadowVuetify エレベーション 8 ²0 4px 16px rgba(0, 0, 0, 0.08)
menuItemRadius--tf-menu-item-radius0px6px
menuItemPadding--tf-menu-item-paddingVuetify ³4px 8px
menuItemMinHeight--tf-menu-item-min-heightVuetify ³34px
menuItemActiveBg--tf-menu-item-active-bgVuetify ³rgba(var(--v-theme-on-surface), 0.06)

³ クラシックメニューのリストボックス(角丸、影、リストのパディング)は、現在はスキンのルールです。SkinScope のトークンラッパーが Vuetify 自身の子ルールとリストの間に入るため、ライブラリは上記のトークンを読むスキンルールとして、それらのピクセル値を復元しています。項目のピクセル値はネイティブの Vuetify のままです(フィクスチャは、そこで Vuetify のデフォルト値を計測しています)。4 つの項目トークンは、menu: 'vuetify' の間は効果がありません。

ダイアログ ​

トークンCSS 変数クラシックpastel
dialogShadow--tf-dialog-shadowVCard エレベーション ²0 20px 48px rgba(0, 0, 0, 0.16), 0 2px 6px rgba(0, 0, 0, 0.06)
dialogRadius--tf-dialog-radius4px(VCard)12px
dialogHeaderPadding--tf-dialog-header-padding8px 16px16px 12px 12px 20px
dialogContentPadding--tf-dialog-content-padding16px 24px 24px4px 20px
dialogActionsPadding--tf-dialog-actions-padding8px16px 20px 20px
dialogTitleSize--tf-dialog-title-size1.25rem1.125rem
dialogTitleWeight--tf-dialog-title-weight400600
dialogFieldRadius--tf-dialog-field-radiusフィールドのデフォルト(4px)8px(outlined)
dialogActionBtnMinHeight--tf-dialog-action-btn-min-heightVuetify(auto)38px
dialogActionBtnRadius--tf-dialog-action-btn-radiusVuetify(4px)8px
dialogGridGap--tf-dialog-grid-gap24px16px

フレーム(エディターの枠ラッパー) ​

トークンCSS 変数クラシックpastel
frameBorder--tf-frame-border1px solid rgba(var(--v-border-color), var(--v-border-opacity))、上辺なし完全な border
frameBg--tf-frame-bgtransparentrgb(var(--v-theme-surface))
frameShadow--tf-frame-shadownone0 1px 2px rgba(0, 0, 0, 0.05)

フラットなツールバーのシェイプでは、フレームは辺ごとの差分も描画します(上辺と、border の不透明度を 1.6 倍にした下辺)。この差分はトークンではなくシェイプに属するため、トークンでは変更できません。

画像とリサイズハンドル ​

トークンCSS 変数クラシックpastel
imageRadius--tf-image-radius0.125rem4px
imageBg--tf-image-bgtransparentrgba(var(--v-theme-on-surface), 0.04)
imageOutline--tf-image-outline1px solid rgba(0, 0, 0, 0.25) (選択時)2px solid rgb(var(--v-theme-primary))
imageOutlineHover--tf-image-outline-hovertransparentrgba(var(--v-theme-primary), 0.35)
handleBg--tf-handle-bgrgba(0, 0, 0, 0.5)rgb(var(--v-theme-primary))
handleBgHover--tf-handle-bg-hoverrgba(0, 0, 0, 0.8) (ホバー時)rgb(var(--v-theme-primary))
handleBorder--tf-handle-bordernone2px solid rgb(var(--v-theme-surface))
handleShadow--tf-handle-shadownone0 0 0 1px rgba(0, 0, 0, 0.15)
handleRadius--tf-handle-radius2px50%

画像ツールバーは buttons フラグに従います。quiet シェイプでは quiet のサーフェスが適用され、その 2px/4px の間隔は、画像メニューのフォールバックを持つ --tf-bubble-gap / --tf-bubble-padding を読みます。画像ツールバー専用のトークンはありません。

カタログにないもの(抜け道の領域) ​

設計の概要で挙げた寸法のうち、v1 のカタログにトークンがないものがあります。ツールバーの高さ、フレームの幅、フレームの角丸、ボタンの幅、ボタンのパディング、ダイアログの幅です。上のカタログの表が仕様であり、これらのピクセル値については、抜け道を使って、安定したクラスを直接スタイリングしてください。

到達可能性 ​

上記のすべてのグループが守る、1 つの原則です。

トークンは、シェイプがそのサーフェスを有効にしている場合にのみ描画されます。ネイティブの Vuetify の見た目は、Vuetify のテーマカラーでスタイリングします。

グループごとの内容は次のとおりです。

  • ツールバー、フレーム、画像/ハンドルのクローム — どのシェイプでも有効です。トークンがそれらを描画し、クラシックのセルはトークンがフォールバックするネイティブの見た目です。
  • グループのボックス(groupBg、groupRadius、groupHeight、groupShadow)— groups: 'box' ではセクションは VBtnGroup(0.3.2 の構造: ボタンが連結され、ボックスがエレベーションを持つ)で、ボックスのルールがグループのトークンで描画します。--tf-group-height は 32px の仕様を !important で維持しています(0.3.2 と同様、Vuetify の density ルールと競合するためです)。groups: 'flat' ではセクションは通常の div の並びになります(透明で、影がなく、角丸 0 がシェイプのベースです)。
  • 区切りの罫線(dividerWidth/Height/Color)— 目に見える罫線は groups: 'flat' で使えるもので、ボックスのシェイプでは区切りはサイズ 0 のスペーサーです(ベースルールの margin: 0 4px)。
  • 項目のパディング/間隔 — コンパクトな 6px 8px はフラットなバー(toolbar: 'flat')で、グループ間の間隔はフラットなセクション(groups: 'flat')で有効になります。浮き出したバーはクラシックのメトリクスのままです。
  • ボタン(buttonFg、buttonHover*、buttonActive*、buttonRadius)— buttons: 'vuetify' では効果がありません。ネイティブの Vuetify ボタンが自分で状態を描画するためです。Vuetify のテーマカラー(primary、surface など)でスタイリングしてください。クラシックなシェイプは、意図的にそれらを継承します。
  • メニュー項目のメトリクス(menuItemRadius/Padding/MinHeight/ActiveBg)— menu: 'vuetify' では効果がなく、これらの項目のピクセル値は Vuetify が管理します。メニューのリストボックス(角丸/影/パディング)は、どちらのシェイプでもスキンが描画します(メニューの注記を参照)。
  • ダイアログのやわらかいクローム(dialogActionBtn*、dialogFieldRadius、やわらかいパディング、区切り線がないこと)— dialog: 'soft' で描画されます。dialog: 'classic' では、ダイアログはクラシックなパディングの VCard です。
  • バブルのクローム — 選択、空行、画像のメニューは、どちらのシェイプでもアイコンボタンの 1 行で、バブルのトークンで描画されます。bubble がそれらのマークアップを変えることはなくなりました。

カタログのベースと同じ値での上書きは出力されません。クラシックではないシェイプでは、シェイプ自身のベースが CSS のフォールバックを通じて描画されるため、クラシックの値に「戻す」こと(たとえば buttons: 'quiet' で buttonActiveBg を rgb(var(--v-theme-primary)) に戻すこと)は、何も起こりません。クラシックなシェイプ以外でクラシックに見える値を強制したい場合は、カタログのベースと異なる上書きを設定してください。rgba(var(--v-theme-primary), 1) のような等価なリテラルは、同じように描画され、スキップされません。

トークンは、ネイティブの描画経路には意図的に接続されていません。これにより、すべてがカタログを経由しつつ、default はクラシックな外観とピクセル単位で同一に保たれます。

レシピ ​

pastel を拡張する ​

quiet のシェイプを継承して、いくつかのサーフェスの色を塗り直します。登録した名前は、どのエディターでも使えます。

ts
// main.ts — call once, before any editor mounts
import { registerSkin } from 'tiptapify'

registerSkin('mint', {
  extends: 'pastel',
  colors: {
    toolbarBg: 'rgba(0, 128, 0, 0.05)',   // mint tint over the pastel wash
    handleBg: 'rgb(0, 100, 0)',           // deep green handles
    handleBgHover: 'rgb(0, 128, 0)',
    imageOutline: '2px solid rgb(0, 100, 0)',
    imageOutlineHover: 'rgba(0, 100, 0, 0.35)',
  },
})
vue
<Tiptapify skin="mint" />

シェイプ(flat/quiet/soft)は pastel から引き継がれ、触れていないトークンは pastel の値のままです。quiet ボタンの背景はニュートラルなままで、上書きしたものだけが変わります。このスキンは、例のページでライブ表示されています。

default を拡張する ​

クラシックの構造はそのままに、色を調整します。

ts
registerSkin('paper', {
  extends: 'default',
  colors: { toolbarBg: 'rgb(var(--v-theme-background))' },
  metrics: { groupRadius: '8px', itemsPadding: '10px 14px' },
})

extends: 'default' は暗黙のルートなので、キーは省略できます。{ toolbar: 'flat', … } もクラシックから始まります。

完全なカスタム ​

extends も登録も不要です。オブジェクトを skin prop に渡し、シェイプのフラグとトークンを自由に設定します。name を指定すると、ルートのクラスに独自のラベルが付きます(後述)。指定しない場合は tiptapify-skin-inline になります。

vue
<script setup lang="ts">
import { defineSkin } from 'tiptapify'

const slate = defineSkin({
  name: 'slate',
  toolbar: 'flat',
  groups: 'flat',
  buttons: 'quiet',
  bubble: 'flat',
  dialog: 'soft',
  menu: 'soft',
  colors: { toolbarBg: 'rgb(30, 41, 59)', toolbarFg: 'rgb(226, 232, 240)' },
  metrics: { buttonRadius: '8px' },
})
</script>

<template>
  <Tiptapify :skin="slate" />
</template>

defineSkin は、型付きの const 定義のための恒等ヘルパーです(レジストリには登録されません)。シェイプのフラグは個別に設定でき、省略したフラグはすべてクラシックのままです。

抜け道: ルートクラス ​

すべてのエディターのルートには tiptapify-skin-{name} が付きます(名前のないインラインオブジェクトでは tiptapify-skin-inline)。ライブラリは、スキン名をキーとするルールを一切持ちません。このクラスは自由に使えます。

css
/* Style the frame of one skin, dimensions the catalog does not cover */
.tiptapify-skin-mint .tiptapify-frame {
  max-width: 720px;
  margin-inline: auto;
}

スタイルを書く際に使える、安定したフックは次のとおりです。

クラス場所
tiptapify-skin-{name}エディターのルート
tiptapify-shape-{flag}-{value} ×6エディターのルート — 解決されたシェイプ
tiptapify-toolbar · tiptapify-toolbar-items · tiptapify-toolbar-group · menu-dividerツールバーとそのセクション
tiptapify-quiet-buttonsbuttons: 'quiet' のときのツールバーとバブルのルート
tiptapify-bubble-row · tiptapify-bubble-dividerすべてのバブルメニューの行と、そのグループ間の罫線
tiptapify-bubble-menu · floating-menu · tiptapify-image-menu選択、空行、画像のメニューの行
tiptapify-btn--activeバブルメニューで押された状態のボタン(アクティブなマーク、画像の回り込み)
tiptapify-menu · tiptapify-menu--softドロップダウンのオーバーレイの内容と、やわらかいメニュールールの担い手
tiptapify-skin-scopeテレポートされたメニュー内のトークンスコープの div
tiptapify-dialog · tiptapify-dialog--softダイアログカードと、そのやわらかい担い手
tiptapify-frameエディターの枠ラッパー

利用側テーマへの防御。 スキンレイヤーは、Vuetify の内部要素にパディングを付けたり色を塗り直したりする管理画面テーマにも耐えられるよう作られています。ルールはシェイプ/キャリアのクラスで限定され、詳細度のために二重のクラス(.v-list.v-list、.v-btn.v-btn.v-btn)を使い、!important は、ベースのコミットで管理画面テーマのケースが必要だと証明した箇所(menu.css の防御セット)にだけ残しています。同じ競合の場で、独自の上書きを機能させ続けるための指針は次のとおりです。

  • CSS は .tiptapify-skin-{name}(またはシェイプのクラス)の下にスコープする。素の Vuetify 内部要素には書かない。
  • カタログにある寸法にはトークン(--tf-*)を、それ以外にはクラスを使う。
  • Vuetify コンポーネントを上書きするときは、二重化に合わせる(単独の .v-list-item は、スキンのルールに負けます)。

ビジュアル仕様(コントリビューター向けメモ) ​

組み込みスキンの算出スタイルは、ベースラインのフィクスチャ src/skins/__fixtures__/skin-baseline.json(13 セクション × ライト/ダーク)に固定されています。計測したマトリクスがこれと一致すれば、CSS レイヤーは完成です。フィクスチャを読む際のメモは次のとおりです。

  • ホバー/アクティブの寸法はページから強制できないため、記録では各ホバーセレクターの最初に一致した CSSOM ルールの宣言テキストを読み取り、スキンのルート内にあるプローブ要素を通じて解決します(resolved フィールド)。これは実際の :hover のスナップショットではなく、勝ったルールの仕様です。
  • _meta.selectors.menu には、SkinScope 導入前のセレクターテキストが記録されています。フィクスチャは凍結されている(規範となる値)ため、このフィールドは意図的に古いままで、記録バンドルが現在のセレクターを固定しています。
  • フィクスチャは、承認済みの変更を _meta.notes に持っています。groups: 'box' で VBtnGroup のマークアップを復元した 2026-10-18 の変更で、クラシックの groupBtn/buttonsActive の子ボタンのフィールド(連結された角、エレベーションを持つのはボックス)を再計測しました。pastel のフィールドは変更されていません。意図的な見た目の変更では、影響を受けるクラシックのセルだけを更新し、メモを記録し、CHANGELOG に 1 行追加します。

関連項目 ​

MIT ライセンスのもとで公開されています。