スタジオ・アルカナの しかた です。
最近業務で、アプリに Markdown プレビュー機能を作る際にアプリ全体の CSS が干渉してデザインが崩れそうという問題がありました。
- コードブロックや 引用箇所 のデザインが想定と違う
- 画面ごとに“違う見た目の Markdown ビューア”を出したいのに、お互いが干渉する
- コンポーネントを分けても アプリ全体にかかったCSS の影響が避けられない
こういったことは割とよくある悩みだと思います。
Markdown表示の UI を安定させるために、Shadow DOM と Tailwind Typography を使ってみたら扱いやすかった、という話です。
Shadow DOM の便利さに気づいた話
AIチャット機能 を実装している中で、LLM が返す Markdown のレイアウト崩れ対策として Shadow DOM を導入しました。
そこで気づいたのがMarkdown 表示のUIにおいて、とても便利なのでは…ということ。
Markdown 表示部分を Shadow DOM で隔離し、
その中に Markdown パーサ が生成した HTML と Tailwind Typography を適用するだけで、
- 他の CSS の影響を完全に遮断
- 複数 Markdown ビューアを別々の見た目にすることも可能
- 記事・プレビュー・チャット・ドキュメントなど用途を問わず安定
と、非常に扱いやすくなりました。
Shadow DOMについて
Shadow DOM は、ブラウザが提供する DOM と CSS のカプセル化機能です。
- Shadow DOM 内のスタイルは外側に漏れない
- 外側の CSS も内側に干渉しない
Vue・React でも簡単に使えるのが特徴です。
Tailwind Typography との相性
https://github.com/tailwindlabs/tailwindcss-typography
https://github.com/markdown-it/markdown-it
Markdown パーサ(markdown-itなど)で HTML を生成し、そこにTailwind Typography (@tailwindcss/typography) を適用すると、以下のような 要素が “いい感じ” に整います。
- 見出し(h1, h2, h3…)
- テーブル
- 引用(blockquote)
- コードブロック
- 画像
- リスト
Shadow DOM 内部で Typography を適用すれば、それだけで十分綺麗な外側のデザインに影響されない Markdown ビューアを作ることができました。
この構成のメリット
- 壊れにくい Markdown ビューアが作れる
- 複数の Markdown スタイルを共存させられる
- 外部 CSS の影響を完全に遮断できる
- 必要な部分だけ独自デザインに変更しやすい
Markdown を扱う UI ではかなり汎用的に使えそうです。
実装例
ここでは Vue での最小構成の例を紹介します。
表示キャプチャ
markdown-shadow-view.vue
<script setup lang="ts">
import MarkdownIt from "markdown-it";
import { computed, onMounted, ref } from "vue";
import markdownStyles from "./markdown-shadow-view.css?inline";
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
});
const markdownInput = `
# Header h1
## Header h2
### Header h3...
`;
// markdown-itのrenderメソッドでMarkdownをHTMLに変換
const renderedHtml = computed(() => md.render(markdownInput));
// Shadow DOMのホスト要素
const shadowHost = ref<HTMLDivElement>();
onMounted(() => {
if (!shadowHost.value) return;
const shadowRoot = shadowHost.value.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>${markdownStyles}</style>
<div class="markdown-content">${renderedHtml.value}</div>
`;
});
</script>
<template>
<div ref="shadowHost"></div>
</template>
<script setup lang="ts">
import MarkdownIt from "markdown-it";
import { computed, onMounted, ref } from "vue";
import markdownStyles from "./markdown-shadow-view.css?inline";
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
});
const markdownInput = `
# Header h1
## Header h2
### Header h3...
`;
// markdown-itのrenderメソッドでMarkdownをHTMLに変換
const renderedHtml = computed(() => md.render(markdownInput));
// Shadow DOMのホスト要素
const shadowHost = ref<HTMLDivElement>();
onMounted(() => {
if (!shadowHost.value) return;
const shadowRoot = shadowHost.value.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>${markdownStyles}</style>
<div class="markdown-content">${renderedHtml.value}</div>
`;
});
</script>
<template>
<div ref="shadowHost"></div>
</template>
markdown-shadow-view.css
@import "tailwindcss";
@plugin "@tailwindcss/typography";
/* Typographyプラグインのproseスタイルを適用 */
.markdown-content {
@apply prose;
}
/* 画像サイズのカスタマイズ */
.markdown-content img {
display: inline-block;
width: 30%;
}
@import "tailwindcss";
@plugin "@tailwindcss/typography";
/* Typographyプラグインのproseスタイルを適用 */
.markdown-content {
@apply prose;
}
/* 画像サイズのカスタマイズ */
.markdown-content img {
display: inline-block;
width: 30%;
}
コードについて
- Markdown パーサで Markdown → HTML へ変換
- 変換後の HTML を Shadow DOM の内部へ挿入
- Shadow DOM 内だけに Tailwind Typography のスタイルを適用
- 外側の CSS は完全にシャットアウト
まとめ
チャット機能に限らず、管理画面のプレビュー、ドキュメント、ブログ編集など、幅広い用途で再利用できそうだと感じました。
備忘録的な内容になってしまいましたが、最後まで読んでいただきありがとうございます!