Status Indicator
A presence dot anchored to an avatar, with online, idle, do-not-disturb, offline and streaming states.
Presence, encoded in shape as well as colour: idle is a crescent, do-not-disturb is a bar, offline is a hollow ring. That is deliberate — the states stay distinguishable in greyscale, in a screenshot, and to a reader who cannot tell green from red.
newt-statusimport { Avatar } from "@/registry/bases/newt/ui/avatar"
import {
StatusDot,
StatusIndicator,
} from "@/registry/bases/newt/ui/status-indicator"
export default function StatusIndicatorDemo() {
return (
<div className="flex items-center gap-6">
<StatusIndicator aria-label="Online">
<Avatar>A</Avatar>
<StatusDot status="online" />
</StatusIndicator>
<StatusIndicator aria-label="Idle">
<Avatar>B</Avatar>
<StatusDot status="idle" />
</StatusIndicator>
<StatusIndicator aria-label="Do not disturb">
<Avatar>C</Avatar>
<StatusDot status="dnd" />
</StatusIndicator>
<StatusIndicator aria-label="Offline">
<Avatar>D</Avatar>
<StatusDot status="offline" />
</StatusIndicator>
</div>
)
}<script setup lang="ts">
import { Avatar } from "~~/registry/bases/newt/ui/avatar"
import {
StatusDot,
StatusIndicator,
} from "~~/registry/bases/newt/ui/status-indicator"
</script>
<template>
<div class="flex items-center gap-6">
<StatusIndicator aria-label="Online">
<Avatar>A</Avatar>
<StatusDot status="online" />
</StatusIndicator>
<StatusIndicator aria-label="Idle">
<Avatar>B</Avatar>
<StatusDot status="idle" />
</StatusIndicator>
<StatusIndicator aria-label="Do not disturb">
<Avatar>C</Avatar>
<StatusDot status="dnd" />
</StatusIndicator>
<StatusIndicator aria-label="Offline">
<Avatar>D</Avatar>
<StatusDot status="offline" />
</StatusIndicator>
</div>
</template>Installation
pnpm dlx newtui@latest add status-indicatornpx newtui@latest add status-indicatoryarn dlx newtui@latest add status-indicatorbunx newtui@latest add status-indicatorInstall the dependencies
pnpm dlx --yes class-variance-authoritynpx --yes class-variance-authorityyarn dlx --yes class-variance-authoritybunx --yes class-variance-authorityAdd the components it builds on
Copy the source into your project
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"
const PULSE_KEYFRAMES = `@keyframes newt-presence-pulse{0%{transform:scale(.7);opacity:.6}70%{transform:scale(1.6);opacity:0}100%{transform:scale(1.6);opacity:0}}`
const StatusIndicator = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
role="status"
className={cn("relative inline-block", className)}
{...props}
/>
))
StatusIndicator.displayName = "StatusIndicator"
const statusDotVariants = cva(
"absolute -bottom-0.5 -end-0.5 h-3.5 w-3.5 rounded-full border-[3px] border-newt-bg-base",
{
variants: {
status: {
online:
"bg-newt-online after:pointer-events-none after:absolute after:-inset-1 after:rounded-full after:border-[1.5px] after:border-newt-online after:opacity-0 after:content-[''] after:[animation:newt-presence-pulse_2.4s_ease-out_infinite] motion-reduce:after:[animation:none]",
idle: "bg-newt-bg-base after:absolute after:inset-0 after:rounded-full after:bg-newt-idle after:content-[''] after:[clip-path:circle(60%_at_35%_35%)]",
dnd: "bg-newt-bg-base after:absolute after:start-1/2 after:top-1/2 after:h-[22%] after:w-[65%] after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-[1px] after:bg-newt-dnd after:content-['']",
offline: "bg-newt-offline",
streaming: "bg-[#593695]",
},
},
defaultVariants: {
status: "offline",
},
}
)
export interface StatusDotProps
extends React.ComponentProps<"div">, VariantProps<typeof statusDotVariants> {}
const StatusDot = React.forwardRef<HTMLDivElement, StatusDotProps>(
({ className, status, ...props }, ref) => (
<>
{status === "online" ? (
<style dangerouslySetInnerHTML={{ __html: PULSE_KEYFRAMES }} />
) : null}
<div
ref={ref}
data-status={status ?? "offline"}
className={cn(statusDotVariants({ status }), className)}
{...props}
/>
</>
)
)
StatusDot.displayName = "StatusDot"
export { StatusIndicator, StatusDot, statusDotVariants }<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div role="status" :class="cn('relative inline-block', props.class)">
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { statusDotVariants, type StatusDotVariants } from "."
const props = withDefaults(
defineProps<{
status?: StatusDotVariants["status"]
class?: HTMLAttributes["class"]
}>(),
{ status: "offline" }
)
</script>
<template>
<div
:data-status="props.status"
:class="cn(statusDotVariants({ status: props.status }), props.class)"
/>
</template>
<style>
@keyframes newt-presence-pulse {
0% {
transform: scale(0.7);
opacity: 0.6;
}
70% {
transform: scale(1.6);
opacity: 0;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
</style>import { cva, type VariantProps } from "class-variance-authority"
export { default as StatusIndicator } from "./StatusIndicator.vue"
export { default as StatusDot } from "./StatusDot.vue"
export const statusDotVariants = cva(
"absolute -bottom-0.5 -end-0.5 h-3.5 w-3.5 rounded-full border-[3px] border-newt-bg-base",
{
variants: {
status: {
online:
"bg-newt-online after:pointer-events-none after:absolute after:-inset-1 after:rounded-full after:border-[1.5px] after:border-newt-online after:opacity-0 after:content-[''] after:[animation:newt-presence-pulse_2.4s_ease-out_infinite] motion-reduce:after:[animation:none]",
idle: "bg-newt-bg-base after:absolute after:inset-0 after:rounded-full after:bg-newt-idle after:content-[''] after:[clip-path:circle(60%_at_35%_35%)]",
dnd: "bg-newt-bg-base after:absolute after:start-1/2 after:top-1/2 after:h-[22%] after:w-[65%] after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-[1px] after:bg-newt-dnd after:content-['']",
offline: "bg-newt-offline",
streaming: "bg-[#593695]",
},
},
defaultVariants: {
status: "offline",
},
}
)
export type StatusDotVariants = VariantProps<typeof statusDotVariants>Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { StatusIndicator } from "@/components/ui/status-indicator"
export function Example() {
return <StatusIndicator />
}<script setup lang="ts">
import { StatusIndicator, StatusDot } from "@/components/ui/status-indicator"
</script>
<template>
<StatusIndicator />
</template>API reference
StatusDot
status?"online" | "idle" | "dnd" | "offline" | "streaming"
The presence state.
"online" | "idle" | "dnd" | "offline" | "streaming""offline"StatusIndicator
status?"online" | "idle" | "dnd" | "offline" | "streaming"
Passed through to the dot.
"online" | "idle" | "dnd" | "offline" | "streaming""offline"label?string
Text beside the dot. Also the accessible name when present.
stringEvery part also accepts the native attributes of the element it renders, and a className that is merged rather than replaced.
Accessibility
The dot carries role="status" and an aria-label naming the state, so it is never colour alone. The online state has a soft pulse, which is the one animation in the library; it is behind prefers-reduced-motion and the state is legible without it.
Tokens
Status Indicator reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-status.