Input
Preview
Code
<script setup lang="ts">
import NumberFlow from '@number-flow/vue'
import { Minus, Plus } from 'lucide-vue-next'
import { ref, useTemplateRef } from 'vue'
const { min = 0, max = 99 } = defineProps<{
min?: number
max?: number
}>()
const modelValue = defineModel({ default: 0 })
const defaultValue = modelValue.value
const inputRef = useTemplateRef('input')
const animated = ref(true)
// Hide the caret during transitions so you can't see it shifting around:
const showCaret = ref(true)
function handleInput({ currentTarget }: Event) {
const input = currentTarget as HTMLInputElement // nicer than inputRef.value.value
animated.value = false
let next = modelValue.value
if (input.value === '') {
next = defaultValue
} else {
const num = input.valueAsNumber
if (!isNaN(num) && min <= num && num <= max) next = num
}
// Manually update the input.value in case the number stays the same e.g. 09 == 9
input.value = String(next)
modelValue.value = next
}
function handlePointerDown(event: PointerEvent, diff: number) {
animated.value = true
if (event.pointerType === 'mouse') {
event?.preventDefault()
inputRef.value?.focus()
}
const newVal = Math.min(Math.max(modelValue.value + diff, min), max)
modelValue.value = newVal
}
</script>
<template>
<div
class="focus-within:ring-accent group flex items-stretch rounded-md text-3xl font-semibold ring ring-zinc-200 transition-[box-shadow] focus-within:ring-2 dark:ring-zinc-800"
>
<button
aria-hidden="true"
tabindex="{-1}"
class="flex items-center pl-[.5em] pr-[.325em]"
:disabled="min != null && modelValue <= min"
@pointerdown="handlePointerDown($event, -1)"
>
<Minus class="size-4" absoluteStrokeWidth strokeWidth="3.5" />
</button>
<div
class="relative grid items-center justify-items-center text-center [grid-template-areas:'overlap'] *:[grid-area:overlap]"
>
<input
ref="input"
:class="[
showCaret ? 'caret-primary' : 'caret-transparent',
'spin-hide w-[1.5em] bg-transparent py-2 text-center font-[inherit] text-transparent outline-none'
]"
:style="{ fontKerning: 'none' /* match NumberFlow */ }"
type="number"
:min
step="1"
autocomplete="off"
inputmode="numeric"
:max
:value="modelValue"
@input="handleInput"
/>
<NumberFlow
:value="modelValue"
locales="en-US"
:format="{ useGrouping: false }"
aria-hidden="true"
:animated
@animationsstart="showCaret = false"
@animationsfinish="showCaret = true"
class="pointer-events-none"
willChange
/>
</div>
<button
aria-hidden="true"
tabindex="-1"
class="flex items-center pl-[.325em] pr-[.5em]"
:disabled="max != null && modelValue >= max"
@pointerdown="handlePointerDown($event, 1)"
>
<Plus class="size-4" absoluteStrokeWidth strokeWidth="3.5" />
</button>
</div>
</template>Activity
Preview
Code
<script setup lang="ts">
import NumberFlow, { type Format, continuous } from '@number-flow/vue'
import { Bookmark, ChartNoAxesColumn, Heart, Repeat, Share } from 'lucide-vue-next'
const format: Format = {
notation: 'compact',
compactDisplay: 'short',
roundingMode: 'trunc'
}
const { likes, reposts, views, bookmarks, liked, reposted, bookmarked } = defineProps<{
likes: number
reposts: number
views: number
bookmarks: number
liked: boolean
reposted: boolean
bookmarked: boolean
}>()
const emit = defineEmits<{
(e: 'like'): void
(e: 'repost'): void
(e: 'bookmark'): void
}>()
</script>
<template>
<div class="flex w-full select-none items-center text-zinc-600 dark:text-zinc-300">
<div class="flex flex-1 items-center gap-1.5">
<ChartNoAxesColumn absoluteStrokeWidth class="~size-4/5" />
<NumberFlow willChange :plugins="[continuous]" :value="views" locales="en-US" :format />
</div>
<div class="flex-1">
<button
:class="[
'group flex items-center gap-1.5 pr-1.5 transition-[color] hover:text-emerald-500',
reposted && 'text-emerald-500'
]"
@click="emit('repost')""
>
<div class="relative before:absolute before:-inset-2.5 before:rounded-full before:transition-[background-color] before:group-hover:bg-emerald-500/10">
<Repeat
absoluteStrokeWidth
class="~size-4/5 group-active:spring-duration-[25] spring-bounce-50 spring-duration-300 transition-transform group-active:scale-[85%]"
/>
</div>
<NumberFlow willChange :plugins="[continuous]" :value="reposts" locales="en-US" :format />
</button>
</div>
<div class="flex-1">
<button
:class="[
'group flex items-center gap-1.5 pr-1.5 transition-[color] hover:text-pink-500',
liked && 'text-pink-500'
]"
@click="emit('like')"
>
<div class="relative before:absolute before:-inset-2.5 before:rounded-full before:transition-[background-color] before:group-hover:bg-pink-500/10">
<Heart
absoluteStrokeWidth
:class="[
'~size-4/5 group-active:spring-duration-[25] spring-bounce-[65] spring-duration-300 transition-transform group-active:scale-[80%]',
liked && 'fill-current'
]"
/>
</div>
<NumberFlow willChange :plugins="[continuous]" :value="likes" locales="en-US" :format />
</button>
</div>
<div class="flex shrink-0 min-[30rem]:flex-1 items-center gap-1.5 max-[24rem]:hidden">
<button
:class="[
'group flex items-center gap-1.5 pr-1.5 transition-[color] hover:text-blue-500',
bookmarked && 'text-blue-500'
]"
@click="emit('bookmark')"
>
<div class="relative before:absolute before:-inset-2.5 before:rounded-full before:transition-[background-color] before:group-hover:bg-blue-500/10">
<Bookmark
absoluteStrokeWidth
:class="[
'~size-4/5 group-active:spring-duration-[25] spring-bounce-50 spring-duration-300 transition-transform group-active:scale-[85%]',
bookmarked && 'fill-current'
]"
/>
</div>
<NumberFlow class="max-[30rem]:hidden" willChange :plugins="[continuous]" :value="bookmarks" locales="en-US" :format />
</button>
</div>
<Share absoluteStrokeWidth class="~size-4/5 shrink-0" />
</div>
</template>Countdown
Preview
Code
<script setup lang="ts">
import NumberFlow, { NumberFlowGroup } from '@number-flow/vue'
import { computed } from 'vue'
const { seconds } = defineProps<{ seconds: number }>()
const hh = computed(() => Math.floor(seconds / 3600))
const mm = computed(() => Math.floor((seconds % 3600) / 60))
const ss = computed(() => seconds % 60)
</script>
<template>
<NumberFlowGroup>
<div
style="font-variant-numeric: tabular-nums"
class="~text-3xl/4xl flex items-baseline font-semibold"
>
<NumberFlow :trend="-1" :value="hh" :format="{ minimumIntegerDigits: 2 }" />
<NumberFlow
prefix=":"
:trend="-1"
:value="mm"
:digits="{ 1: { max: 5 } }"
:format="{ minimumIntegerDigits: 2 }"
/>
<NumberFlow
prefix=":"
:trend="-1"
:value="ss"
:digits="{ 1: { max: 5 } }"
:format="{ minimumIntegerDigits: 2 }"
/>
</div>
</NumberFlowGroup>
</template>