Badge

Pins an unread count or short marker to the corner of its host.

399+NEW
<script setup lang="ts">
  import { Bell, Mail, MessageSquare } from '@lucide/vue'
  import { Badge, IconButton, Inline } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Badge :content="3">
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
    <Badge :content="128" tone="accent">
      <IconButton label="Messages" variant="soft">
        <Mail />
      </IconButton>
    </Badge>
    <Badge content="NEW" tone="success" size="md">
      <IconButton label="Comments" variant="soft">
        <MessageSquare />
      </IconButton>
    </Badge>
  </Inline>
</template>
vue

Usage

import { Badge } from '@hina-ui/vue'
ts

The host element goes in the default slot and content is pinned to its corner. The component only positions and renders the badge; the host itself is untouched. The badge ignores pointer events by default, so the host remains interactive where the badge overlaps it.

55 unread
<script setup lang="ts">
  import { Bell } from '@lucide/vue'
  import { Badge, IconButton } from '@hina-ui/vue'
</script>

<template>
  <Badge :content="5" label="5 unread">
    <IconButton label="Notifications" variant="soft">
      <Bell />
    </IconButton>
  </Badge>
</template>
vue

Examples

Maximum

When content is a number greater than max, it is shown as the limit followed by a plus sign. The default limit is 99.

9999+999+
<script setup lang="ts">
  import { Bell } from '@lucide/vue'
  import { Badge, IconButton, Inline } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Badge :content="99">
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
    <Badge :content="1280">
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
    <Badge :content="1280" :max="999">
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
  </Inline>
</template>
vue

Tones

Defaults to danger. Use danger for unread and pending items, accent or neutral for ordinary counts, and the remaining three by their meaning.

666666
<script setup lang="ts">
  import { Bell } from '@lucide/vue'
  import { Badge, IconButton, Inline } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Badge
      v-for="tone in ['danger', 'accent', 'neutral', 'success', 'warning', 'info'] as const"
      :key="tone"
      :content="6"
      :tone="tone"
    >
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
  </Inline>
</template>
vue

Sizes

Both sizes use the same font size; they differ in the height and minimum width of the badge. Use md for larger hosts or textual content.

88
<script setup lang="ts">
  import { Bell } from '@lucide/vue'
  import { Badge, IconButton, Inline } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Badge :content="8" size="sm">
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
    <Badge :content="8" size="md">
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
  </Inline>
</template>
vue

Placement

placement selects the corner, with four values. The default is the top end corner.

2222
<script setup lang="ts">
  import { Bell } from '@lucide/vue'
  import { Badge, IconButton, Inline } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Badge
      v-for="placement in ['top-end', 'top-start', 'bottom-end', 'bottom-start'] as const"
      :key="placement"
      :content="2"
      :placement="placement"
    >
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
  </Inline>
</template>
vue

Round hosts

Set shape="circle" for a round host. The anchor point moves inward so the badge sits against the arc rather than the corner of the bounding box.

Shion Hoshimi4

rect

Shion Hoshimi4

circle

<script setup lang="ts">
  import { Avatar, Badge, Inline, Stack, Text } from '@hina-ui/vue'
</script>

<template>
  <Inline align="start" class="gap-8">
    <Stack gap="xs" align="center">
      <Badge :content="4">
        <Avatar size="lg" src="/avatars/peek.webp" alt="Shion Hoshimi" />
      </Badge>
      <Text tone="muted" size="sm">rect</Text>
    </Stack>
    <Stack gap="xs" align="center">
      <Badge :content="4" shape="circle">
        <Avatar size="lg" src="/avatars/peek.webp" alt="Shion Hoshimi" />
      </Badge>
      <Text tone="muted" size="sm">circle</Text>
    </Stack>
  </Inline>
</template>
vue

Custom content

The content slot replaces what the badge shows, for example an icon.

Whether the badge renders is still decided by the content property, so it must be set to a non-empty value when the slot is used. Provide an accessible name through label in that case.

Done
<script setup lang="ts">
  import { Bookmark, Check } from '@lucide/vue'
  import { Badge, IconButton } from '@hina-ui/vue'
</script>

<template>
  <Badge content="Done" tone="success" size="md" label="Done">
    <template #content>
      <Check class="size-3" />
    </template>
    <IconButton label="Bookmark" variant="soft">
      <Bookmark />
    </IconButton>
  </Badge>
</template>
vue

With bare set the badge draws no background or padding and only positions and outlines its content, which decides the size. Put an Indicator in the slot to show a status dot on the corner of an avatar.

Hoshimi ShionOnlineHSAway
<script setup lang="ts">
  import { Avatar, Badge, Indicator, Inline } from '@hina-ui/vue'
</script>

<template>
  <Inline gap="lg">
    <Badge content="online" bare label="Online" placement="bottom-end" shape="circle">
      <template #content>
        <Indicator tone="success" size="lg" />
      </template>
      <Avatar src="/avatars/paper.webp" alt="Hoshimi Shion" />
    </Badge>
    <Badge content="away" bare label="Away" placement="bottom-end" shape="circle">
      <template #content>
        <Indicator tone="warning" size="lg" />
      </template>
      <Avatar name="Hoshimi Shion" />
    </Badge>
  </Inline>
</template>
vue

Outline

outline is on by default and draws a ring in the page background colour, cutting the badge out of the host’s edge. Turn it off over darker hosts.

99
<script setup lang="ts">
  import { Bell } from '@lucide/vue'
  import { Badge, IconButton, Inline } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Badge :content="9">
      <IconButton label="Notifications" variant="solid" tone="accent" pill size="lg">
        <Bell />
      </IconButton>
    </Badge>
    <Badge :content="9" :outline="false">
      <IconButton label="Notifications" variant="solid" tone="accent" pill size="lg">
        <Bell />
      </IconButton>
    </Badge>
  </Inline>
</template>
vue

Appearing and disappearing

The badge does not render when content is 0, an empty string or unset. Both appearing and disappearing scale and fade.

22 unread
<script setup lang="ts">
  import { ref } from 'vue'
  import { Bell } from '@lucide/vue'
  import { Badge, Button, IconButton, Inline } from '@hina-ui/vue'

  const count = ref(2)
</script>

<template>
  <Inline align="center" class="gap-6">
    <Badge :content="count" :label="`${count} unread`">
      <IconButton label="Notifications" variant="soft">
        <Bell />
      </IconButton>
    </Badge>
    <Inline align="center">
      <Button variant="outline" tone="neutral" size="sm" @click="count = Math.max(0, count - 1)">
        Fewer
      </Button>
      <Button variant="outline" tone="neutral" size="sm" @click="count += 1">More</Button>
    </Inline>
  </Inline>
</template>
vue

Accessibility

  • A bare number lacks context, so use label to spell it out, for example “3 unread”. That text is read by screen readers only and is not shown on screen.
  • The component does not change the semantics of its host. A button host is still a button.

API

Props

Prop
Type
Default
Description
content
string | number | null
—
Badge content; nothing renders when empty
max
number
99
Upper limit for numeric content
tone
'danger' | 'accent' | 'neutral' | 'success' | 'warning' | 'info'
'danger'
Semantic tone
size
'sm' | 'md'
'sm'
Size
placement
'top-end' | 'top-start' | 'bottom-end' | 'bottom-start'
'top-end'
Which corner to pin to
shape
'rect' | 'circle'
'rect'
Shape of the host
outline
boolean
true
Draw a ring in the page background colour
bare
boolean
false
Only position, without a background
label
string
—
Full wording for screen readers
class
string
—
Classes appended to the wrapper

Slots

Slot
Description
default
The host element
content
Replaces the badge content