この記事でかいたこと
1.コンポーネントとは?
2.Nuxt/vue
3.vueのコンポーネント
①フッターを下に固定する。(TailwindCSS)
②フッターのアイコンを押したら画面遷移する。(routeとrouter)
③表示したい部分以外を透明化(transparent)
④^ボタンのサイズのそろえ方(Lucide)
⑤画面内でアイコンを移動させる方法
目次
1.はじめに
2.担当部分と学んだことcomponents編
3.まとめ
4.おわりに
1. はじめに
2025秋プロで自分がさわった所、それを作るときに悩んでエラーでまくったけど最終解決した点を忘れないためにまとめておきます。普段はiPadにメモしているけどコード手書きはさすがにきついのでここに書こうと思います。教えていただいたことが多すぎて長くなっちゃってます。わかったつもりになっていることを見つけるには人に説明すると良いときいたので、記事にします。
コンポーネントとページの2か所をさわったので、まずはコンポーネントについて。前編です。
プロジェクト概要
プロジェクト名
MeetUP+R
概要説明
OIC(立命館大学大阪いばらきキャンパス)の学生間、特に留学生と日本人学生の交流を促進するコミュニティアプリ。 共通の趣味や言語を通じて、自然な出会いや会話のきっかけを作り出す。
言語とフレームワーク
言語はType script、フレームワークはNuxt 4です。
これはコンポーネントでアプリケーションを構築します。
コンポーネントとは機能ごとにわけた小さな部品のことです。
例:ロボットを組み立てるイメージ。
ロボットを作るとき、頭や腕などを一気に作ろうとするよりも、足だけ、腕だけ、とそれぞれを先に作っておいて、最後に組み立てるほうがいいです。
なぜなら、もし、足の部分がだめだ!となったときに、1か所で作ってしまっていた場合、全部を壊さないといけなくなってしまうかもしれないです。また、上半身を入れ替える、右腕だけ変える、ということも、別の場所で部品ごとに作っていれば、読んでくる場所を変えるだけでできます。
vueはscriptタグの中にJavascriptを、templeteタグの中に見た目の部分であるHTML、styleタグの中にCSSを書く決まりになっています。
けど、今回はTailwindCSSを使用したので、class=の後ろに書くことで、scriptとtempleteの2つのタグだけ使用しました。
私がさわったファイルは大きく分けて2つです。
1.components
- Footer.vue(フッター部分だけ書いたコンポーネント)
- Header.vue(ヘッダー部分だけ書いたコンポーネント)
- MeetDesireSlider.vue(会いたい度の部分だけ書いたコンポーネント)
2.pages(実際に表示する画面/ここに画面の1部としてコンポーネントをよんでくる)
- home.vue(チャット相手一覧ページ)
- serch.vue(検索画面ページ)
- chat/[id].vue(チャット画面ページ、会いたい度はここに表示したい)
- my-profile.vue(自分のプロフィール/編集できるようにする)
- other-profile.vue(相手のプロフィール/チャットに飛ぶボタン作る)
2. 担当部分と学んだことcomponents編
フッター
私は1番最初にフッターを書かせてもらいました。
home.vueにいる場合のフッター、完成イメージデザインです。
デザインする際、アイコンはlucideを使ったので、そのままコードに書くことができました。
まずはscript setupタグから見てみます。
<script setup>
//↓ref:動く変数。値が変わればアイコンの色が変わるようにしたい。selectedの判定。
import { ref } from 'vue'
import { useRouter } from 'vue-router'
//↓ここでlucideから3つのアイコンを持ってきている。
import { Search, MessageCircleMore, UserRound } from 'lucide-vue-next'
// Router(ページ遷移に必要)
//↓こちらは画面遷移をさせる側
//router.push({ name: 'home' }):homeに遷移するときにこんな感じで使えるrouter
const router = useRouter()
//↓現在地を見る側
const route = useRoute()
// 現在のページによって初期設定を決める
const selected = ref('')
//現在地の話をしているので、routeになっている。
//↓updateSelectedByRouteの部分でページ変わった瞬間自動でselected更新している。
const updateSelectedByRoute = () => {
if (route.path === '/search') {
selected.value = 'search'
} else if (route.path === '/my-profile') {
selected.value = 'user'
} else {
// /home など → チャット(message)
selected.value = 'message'
}
}
// 初期実行
updateSelectedByRoute()
// ページ遷移したら自動で更新.route.pathを見てselectedに正しい関数を入れる
//wachは監視役。URLが変わったら毎回update selected by routeを実行して現在地確認する。
watch(() => route.path, () => {
updateSelectedByRoute()
})
// --- ボタン押したとき画面遷移したいからrouter --- //
const onSearch = () => router.push('/search')
const onMessage = () => router.push('/home')
const onUser = () => router.push('/my-profile')
</script>'
次にtempleteタグの中を見てみます。
<template>
<div class="flex justify-around items-center p-3" :class="['bg-[var(--meetupr-sub)]']">
<!-- 検索ボタン -->
<button @click="onSearch" class="p-2 transition">
<Search
class="w-8 h-8"
:class="selected === 'search' ? 'text-[var(--meetupr-color-3)]' : 'text-[var(--meetupr-main)]'"
/>
</button>
<!-- メッセージボタン -->
<button @click="onMessage" class="p-2 transition">
<MessageCircleMore
class="w-8 h-8"
:class="selected === 'message' ? 'text-[var(--meetupr-color-3)]' : 'text-[var(--meetupr-main)]'"
/>
</button>
<!-- ユーザーボタン -->
<button @click="onUser" class="p-2 transition">
<UserRound
class="w-8 h-8"
:class="selected === 'user' ? 'text-[var(--meetupr-color-3)]' : 'text-[var(--meetupr-main)]'"
/>
</button>
</div>
</template>
こんな感じでフッター部分自体は完成しました!
会いたい度
会いたい度の完成イメージはこれです。

左側の^ボタンを押したら右側の小さいものに変えるようになるというものです。
また、チャット画面に表示するので、最小化したものが移動できるようにすることが目標です。
他のメンバーが書いてくれていた会いたい度を修正するのが私の担当でした。
①会いたい度以外の部分まで白くなっている→透明化
これは背景な話だからbackgroundの部分を見ればいいのかな、としか思い浮かびませんでした。
会いたい度の部分はTailwind CSSではなく、CSSで書いてくれていたので、styleタグの中を見ました。(上のvueの説明の所に「styleタグの中にCSSを書く」と書いてる)
AIにきくと、
background: transparent; /* ← ここ */
これを教えてもらいました。開発者ツールでみてみると、背景色を表示しないだけで、幅自体は存在していました。transparent は色を消すだけで、要素の幅・高さやクリック可能領域は消えない
ということを学びました。
②^ボタンが小さい
もともと、^ボタンをただの「^」だけを打っていう状態でした。
それだと、サイズを変えても思うように大きくならなかったり、上が削れてしまったりしました。
そこで先輩が、lucideでやったほうが早いかも、と言ってくださったので取ってくると自由に変えることができました。
どうして^だとそろわないの?
これはフォント依存なので環境差があるらしいです。
文字はフォントごとに文字幅などが違うということです。最初fontsizeを変えまくるという力技が必要になっていたけれど、lucideはSVG(コードでイラストを描いてくれている)なので常に正方形の座標系になっています。だから、テキストも文字列も同じ横並び要素として扱えるということです。
つまり、
フォント文字(^)はフォントごとに太さやベースライン位置が変わるけれど、SVG は固定された図形として描画されるから、レイアウトやサイズが一定になりやすい、ということです。
<div class="title" @click="toggleExpand">
会いたい度 <ChevronUp class="caret" />
</div>
③最小化したものが移動できるようにする
ここが今回いっちばん困ったところ。
画面内で動かすとかしたことなかったし、どこをさわればいいかわからかった。
いったんAIに聞いて帰ってきたコードを読んでも理解できないから写すこともできない。
いったん寝てから落ち着いて考えてみたら、状態は、「最小化ボタンをさわっているとき」と「そうでないとき」の2つだけだと気づいたのでif文をどこに書けばいいかをAIに相談しました。
以下が返ってきた答えの1部↓
①位置管理:dragX,dragY
②反映:minimizedStyle → left / top
③操作:pointerdown → pointermove → pointerup
一旦実際のコードを分けてみていきます。
1.位置管理をしている変数
<div>
const dragX = ref(0)
const dragY = ref(80)
</div>
ボタンの左上の座標を表しています。この値が変わる=ボタンが動く。ということです。
2.実際に位置を反映している場所
const minimizedStyle = computed(() => ({
position: 'fixed',
left: `${dragX.value}px`, // ← 横位置
top: `${dragY.value}px`, // ← 縦位置
zIndex: 9999, //←ここで最小化ボタンを最前列に来るようにしてる。数が大きいほど前。
touchAction: 'none', //ボタンに触れているときはほかの動きができないようにする
//ドラッグ状態でカーソルを切り替える。?の前がif文のイメージ。:の後ろがelse部分。
cursor: isDragging.value ? 'grabbing' : 'grab'
}))
ここが移動の正体でした。1番下の行だけコメントアウトだけでは説明がむずいのでここに書きます。
grabbing:つかんでいる
grab:つかめる状態
3.ドラッグ開始(触れた瞬間)
@pointerdown.prevent="startDrag"
function startDrag(e) {
pointerDown.value = true
startX = e.clientX
startY = e.clientY
}
このeはeventがすべて入った箱
どこで起きた?何のボタン?どのキー?画面のどの位置?
などのすべての情報が入ってます。
今回は触れた瞬間の座標を記録してる。
e.clientX / e.clientY は「カーソルの画面上の位置」を示す値で、それを使って差分を計算しています
流れとしては、
1触れた!
2pointerDown = true(今ドラッグ中だよ)
3押し始めた位置をstartX, startYに保存
4あとでmoveと比べて計算する。
4.6px以上動いたらドラッグ開始
これはクリックじゃなくてドラッグだな、とわかるようにする。
function onPointerMove(e) {
if (pointerDown.value && !isDragging.value) {
const dx0 = Math.abs(e.clientX - startX)
const dy0 = Math.abs(e.clientY - startY)
const threshold = 6
//判定条件
if (dx0 >= threshold || dy0 >= threshold) {
isDragging.value = true
boxStartX = dragX.value
boxStartY = dragY.value
}
}
const dx0 = Math.abs(e.clientX - startX)ここの部分でx方向はどれくらい動いたか計算してます。yも同様です。
ドラッグ終了
function endDrag(e) {
if (isDragging.value) {
isDragging.value = false
justDragged.value = true
localStorage.setItem(STORAGE_KEY, JSON.stringify({
x: dragX.value,
y: dragY.value
}))
}
pointerDown.value = false
}
位置をlocalStorageに保存しているので次回もその位置からアクセスできます。
ここで新たに悩むポイントが出てきました。
パソコンの開発者ツールでスマホサイズにして見てみると画面外に出ていることになっている
とても困りました。もう最終発表までに時間もなかったのでAIに聞いてみると、
dragX.value = Math.min(Math.max(0, nx), vw - boxW)
dragY.value = Math.min(Math.max(0, ny), vh - boxH)
このように返ってきました。
上の1行を例として見てみましょう。
nxは本来置きたいx座標。
Math.max(0, nx)の部分でnxが0未満にならないようにしています。
-20になっているのなら強制的に0に、30ならそのまま30になります。
左端より左に行かないようになってます。
vwは画面の幅、viewpoint widthのことらしいです。
boxWはボックス自身の幅を表しています。最大x座標を表しています。
Math.min(さっきの値, vw - boxW)
もしnxが大きすぎたら(最大値を超えたら)vw - boxW、収まってたらそのまま。
ということです。
これで、動くようになりました!
3. まとめ
たくさんのことを書きました。
今後同じようなところで躓いたときに見返せるように簡単にまとめます。
1.フッターを画面下に固定する方法
2.route/routerを使った画面遷移
3.背景の透明化やアイコン調整
4.ドラッグ操作による位置管理と状態の分け方
AIに頼る場面も多かったけれど、そのまま写すのではなく、なぜそうなるのか、を理解してからすすめることで理解が以前よりかは深まったと感じています。
4. おわりに
今回はコンポーネント部分についてまとめました。
次のpages編で、画面ごとの役割やコンポーネントの呼び出し方についても整理したいです。
参考
①先輩たちの説明
②Vue.jsとは(2025.12.23)
https://www.webstaff.jp/guide/trend/webit/vuejs/
③cssのtransparentについて理解する(2025.12.23)
https://qiita.com/yamatai12/items/8549fc174e8898eaa83e
