Skip to content
[Vitepress] Vitepress footnote tooltip main image

[Vitepress] Vitepress footnote tooltip

schedule
update
# vitepress
# markdown-it
# footnote
# tooltip

Vitepress에서 각주 기능을 넣고 확인해보니 tooltip처럼 뿅하고 나오는게 없길래 그냥 하나 만들었다. 이게 정답인지 아닌지는 모르겠지만 나는 마음에 든다.

완성 코드 바로가기

1. markdown-it-footnote

우선 vitepress는 markdown-it를 통해 markdown을 html로 변환해주고 있다.
그래서 각주를 달려고 하면 markdown-it-footnote이라는 확장 패키지를 통해 각주 기능을 추가 할 수 있다.

1-1. 설치

bash
npm i -d markdown-it-footnote

# or

yarn add -D markdown-it-footnote

# or

pnpm i -D markdown-it-footnote

# or

bun add markdown-it-footnote -d

사용중인 패키지 매니저 사용해서 적절하게 설치해준다.

1-2. 적용 및 사용

<vitepress root>/.vitepress/config.mts에 가보면 vitepress의 기본적인 설정을 해줄수있다.

ts
...
import footnote from 'markdown-it-footnote'
...
export default defineConfig({
  ...
  markdown: {
    config: (md) => {
      md.use(footnote)
    },
    ...
  },
  ...
})

이렇게 markdown-it 라이브러리에 footnote 기능을 사용하도록 해줄 수 있다.

md
hello[^1]

[^1]: world

hello[1]

이렇게 사용해 줄 수 있다.
하지만 각주 하나 보겠다고 아래 내렸갔다가 위로 다시 올라갔다가 url에는 이상한 id값 계속 붙고...
지저분하다.

2. 툴팁 제작

기본적으로 툴팁이 있어야 붙이든 말든 하기 때문에 codepen에서 적당히 이쁜 툴팁을 찾아왔다.
맘에 안들면 다른거 찾아와서 똑같이 따라하면 된다.
나는 아래 링크의 tooltip을 사용해서 하려고 한다.

tutsplus - CSS Tooltip Magic

2-1. Tooltip Component

각주는 그대로 놔두고 뭔가 추가로 붙여주기만 하면 될거같아서 slot에는 각주를 넣고 그 각주에 tooltip만 붙여주면 될거같다.

vue
<template>
  <slot></slot>
</template>

<script setup lang="ts">
const props = defineProps({
  footnote: {
    type: [Number, String],
    required: true,
  },
})
</script>

<style lang="scss"></style>
ts
...
export default {
  ...,
  enhanceApp({ app, router, siteData }) {
    ...
    app.component('FootnoteTooltip', FootnoteTooltip)
  },
  ...
} satisfies Theme

FootnoteTooltip 컴포넌트를 만들어준 뒤 어디서든 사용하기 위해서 .vitepress/theme/index.ts 전역 컴포넌트로 지정해줬다.
컴포넌트 이름이 너무 긴거 같으면 맘대로 바꿔넣어두면 된다.

<style> 태그에 scope 달아두면 컴포넌트 안에서만 쓸 수 있으니까 지워야한다.

<template> 부분은 각주를 그대로 받아서 사용할거라 default slot만 추가해서 마무리 한다.

prop으로는 굳이 각주를 다 안가지고 와도 문서 하단에 뿌려둘것이기 때문에 굳이 전문을 가지고 오지말고 각주 번호나 이름을 가져오면 될거같다.

2-2. 본문 각주 바로가기에 data-tooltip 추가

각주의 변환된 html 코드를 확인해 보니 아래와 같은 형태인것을 알 수 있다.

html
<a
  href="#fn1"
  id="fnref1"
  >[1]</a
>

...

<li
  id="fn1"
  class="footnote-item"
>
  <p>
    world
    <a
      href="#fnref1"
      class="footnote-backref"
      >↩︎</a
    >
  </p>
</li>

본문에 써지는 <a>태그의 id는 #fnref + footnote
하단에 써지는 <li>태그의 id는 #fn + footnote

와 같은 형태인 것을 확인 할 수 있었다.

그래서 li의 텍스트를 긁어와서 a에 dataset.tooltip으로 넣어주면 나중에 스타일 붙일때 사용할 수 있을거 같다.
아래는 위에서 설명한 내용에 대한 코드다.

vue
<script setup lang="ts">
import { nextTick, onMounted } from 'vue'

const props = defineProps({
  footnote: {
    type: [Number, String],
    required: true,
  },
})

onMounted(() => {
  nextTick(() => {
    // <li> 태그 들고오고
    const fn = document.getElementById(`fn${props.footnote}`)!

    // <a> 태그 들고와서
    const fnref = document.getElementById(`fnref${props.footnote}`)!

    // <li> 클론 하나 만들어 준 뒤
    const fnClone = fn.querySelector('p')?.cloneNode(true) as HTMLElement

    // 뒤에 붙는 화살표 지워주고
    fnClone.removeChild(fnClone.querySelector('a')!)

    // <a> 태그에 data-tooltip에 넣어준다.
    fnref.dataset.tooltip = fnClone.innerText
  })
})
</script>

2-3. 스타일 붙이기

코드펜에서 찾은 맘에 드는 툴팁을 아래 코드처럼 [data-tooltip]에 적절하게 적용시켜 준다.

css
[data-tooltip] {
  /* 여기에 뭔가 하면 각주에 적용될거다 */
}

3. 완성된 코드

md
hello<FootnoteTooltip footnote="2">[^2]</FootnoteTooltip>

[^2]: world

hello[2]

vue
<template>
  <slot></slot>
</template>

<script setup lang="ts">
import { nextTick, onMounted } from 'vue'

const props = defineProps({
  footnote: {
    type: [Number, String],
    required: true,
  },
})

onMounted(() => {
  nextTick(() => {
    const fn = document.getElementById(`fn${props.footnote}`)!
    const fnref = document.getElementById(`fnref${props.footnote}`)!

    const fnClone = fn.querySelector('p')?.cloneNode(true) as HTMLElement
    fnClone.removeChild(fnClone.querySelector('a')!)

    fnref.dataset.tooltip = fnClone.innerText
  })
})
</script>

<style lang="scss">
[data-tooltip] {
  position: relative; /* opinion 1 */

  &::before,
  &::after {
    text-transform: none; /* opinion 2 */
    font-size: 0.9em; /* opinion 3 */
    line-height: 1;
    user-select: none;
    pointer-events: none;
    position: absolute;
    display: none;
    opacity: 0;
  }

  &::before {
    content: '';
    border: 5px solid transparent; /* opinion 4 */
    z-index: 1001; /* absurdity 1 */
  }

  &::after {
    content: attr(data-tooltip); /* magic! */

    /* most of the rest of this is opinion */
    font-family: Helvetica, sans-serif;
    text-align: center;

    /*
      Let the content set the size of the tooltips
      but this will also keep them from being obnoxious
      */
    min-width: 3em;
    white-space: nowrap;
    padding: 1ch 1.5ch;
    border-radius: 0.3ch;
    box-shadow: 0 1em 2em -0.5em rgba(0, 0, 0, 0.35);
    background: #555;
    color: #fff;
    z-index: 1000; /* absurdity 2 */
  }

  &:hover::before,
  &:hover::after {
    display: block;
    animation: tooltips-vert 300ms ease-out forwards;
    left: 50%;
    transform: translate(-50%, -0.5em);
  }

  &::before {
    bottom: 100%;
    border-bottom-width: 0;
    border-top-color: #555;
  }

  &::after {
    bottom: calc(100% + 5px);
  }
}

/* don't show empty tooltips */
[data-tooltip='']::before,
[data-tooltip='']::after {
  display: none !important;
}

/* KEYFRAMES */
@keyframes tooltips-vert {
  to {
    opacity: 1;
    transform: translate(-50%, 0);
  }
}

@keyframes tooltips-horz {
  to {
    opacity: 1;
    transform: translate(0, -50%);
  }
}
</style>

  1. world ↩︎

  2. world ↩︎

Updated at: