Vela UI Docsvenizia.ai

Counter

A compact numeric pill for unread items and pending tasks.

npx shadcn@latest add @velaui/counter
npx shadcn@latest add @velaui/counter

Examples

  • Default
  • Accent low
  • Red high small
  • Blue low small
  • Green high
  • Yellow low overflow

View code

Counter · Default

Installation

pnpm dlx velaui add @velaui/counter
pnpm dlx velaui add @velaui/counter

Code

counter-default.tsx
import { Counter } from "@/components/ui/counter";

export default function Example() {
  return <Counter>3</Counter>;
}

View code

Counter · Accent low

Installation

pnpm dlx velaui add @velaui/counter
pnpm dlx velaui add @velaui/counter

Code

counter-accent-low.tsx
import { Counter } from "@/components/ui/counter";

export default function Example() {
  return (
    <Counter variant="accent" emphasis="low">
      12
    </Counter>
  );
}

View code

Counter · Red high small

Installation

pnpm dlx velaui add @velaui/counter
pnpm dlx velaui add @velaui/counter

Code

counter-red-high-small.tsx
import { Counter } from "@/components/ui/counter";

export default function Example() {
  return (
    <Counter variant="red" size="small">
      9
    </Counter>
  );
}

View code

Counter · Blue low small

Installation

pnpm dlx velaui add @velaui/counter
pnpm dlx velaui add @velaui/counter

Code

counter-blue-low-small.tsx
import { Counter } from "@/components/ui/counter";

export default function Example() {
  return (
    <Counter variant="blue" emphasis="low" size="small">
      5
    </Counter>
  );
}

View code

Counter · Green high

Installation

pnpm dlx velaui add @velaui/counter
pnpm dlx velaui add @velaui/counter

Code

counter-green-high.tsx
import { Counter } from "@/components/ui/counter";

export default function Example() {
  return <Counter variant="green">24</Counter>;
}

View code

Counter · Yellow low overflow

Installation

pnpm dlx velaui add @velaui/counter
pnpm dlx velaui add @velaui/counter

Code

counter-yellow-low-overflow.tsx
import { Counter } from "@/components/ui/counter";

export default function Example() {
  return (
    <Counter variant="yellow" emphasis="low">
      99+
    </Counter>
  );
}

Usage

import { Counter } from "@/components/ui/counter";

export default function Example() {
  return <Counter>3</Counter>;
}

Props

PropTypeDefault
asChildbooleanfalse
emphasis"high" | "low"—
size"small" | "default"—
variant"default" | "red" | "green" | "blue" | "yellow" | "accent"—

Takes these and every prop of CounterProps.

PropTypeDefault
emphasis"high" | "low"—
size"small" | "default"—
variant"default" | "red" | "green" | "blue" | "yellow" | "accent"—

Takes these and every prop of CounterProps.

Dependencies

npm
class-variance-authority@0.7.1radix-ui@1.6.0
Installs with it
nothing else
npm
@base-ui/react@1.6.0class-variance-authority@0.7.1
Installs with it
nothing else

Source

components/ui/counter.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Slot } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

/**
 * Counter — a compact numeric pill (unread items, pending tasks).
 *
 * The design crosses `Appearance` (the colour) with `Emphasis` (solid fill vs
 * tinted surface), so the twelve colour pairs live in `compoundVariants`; the
 * `variants` blocks carry only what a single axis can decide on its own.
 * The design draws no interactive state — this is a display element.
 */
const counterVariants = cva(
  "inline-flex shrink-0 flex-col items-center justify-center overflow-hidden rounded-full px-1 py-0.5 text-center whitespace-nowrap tabular-nums",
  {
    variants: {
      variant: {
        default: "",
        accent: "",
        red: "",
        blue: "",
        green: "",
        yellow: "",
      },
      emphasis: {
        high: "",
        low: "",
      },
      size: {
        // Height comes from padding + line-height: 2 + 16 + 2 = 20.
        default: "min-w-5 text-xs leading-4 font-medium",
        // 2 + 12 + 2 = 16.
        small: "min-w-4 text-[10px] leading-3 font-semibold",
      },
    },
    compoundVariants: [
      {
        variant: "default",
        emphasis: "high",
        className: "bg-bg-foreground text-text-inverse",
      },
      {
        variant: "accent",
        emphasis: "high",
        className: "bg-accent-default text-base-white",
      },
      {
        variant: "red",
        emphasis: "high",
        className: "bg-negative-middle text-base-white",
      },
      {
        variant: "blue",
        emphasis: "high",
        className: "bg-info-middle text-base-white",
      },
      {
        variant: "green",
        emphasis: "high",
        className: "bg-success-middle text-base-white",
      },
      {
        variant: "yellow",
        emphasis: "high",
        className: "bg-warning-middle text-base-white",
      },
      {
        variant: "default",
        emphasis: "low",
        className: "bg-bg-surface-1 text-text-strong",
      },
      {
        variant: "accent",
        emphasis: "low",
        className: "bg-teal-50 text-neutral-950",
      },
      {
        variant: "red",
        emphasis: "low",
        className: "bg-negative text-negative-middle",
      },
      {
        variant: "blue",
        emphasis: "low",
        className: "bg-info text-info-middle",
      },
      {
        variant: "green",
        emphasis: "low",
        className: "bg-success text-success-middle",
      },
      {
        variant: "yellow",
        emphasis: "low",
        className: "bg-warning text-warning-middle",
      },
    ],
    defaultVariants: {
      variant: "default",
      emphasis: "high",
      size: "default",
    },
  },
);

export type CounterProps = React.ComponentProps<"span"> &
  VariantProps<typeof counterVariants> & {
    asChild?: boolean;
  };

function Counter({
  className,
  variant,
  emphasis,
  size,
  asChild = false,
  ...props
}: CounterProps) {
  const Comp = asChild ? Slot.Root : "span";

  return (
    <Comp
      data-slot="counter"
      data-variant={variant}
      data-emphasis={emphasis}
      data-size={size}
      className={cn(counterVariants({ variant, emphasis, size }), className)}
      {...props}
    />
  );
}

export { Counter, counterVariants };
components/ui/counter.tsx
/* eslint-disable react-refresh/only-export-components */
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

/**
 * Counter — a compact numeric pill (unread items, pending tasks).
 *
 * The design crosses `Appearance` (the colour) with `Emphasis` (solid fill vs
 * tinted surface), so the twelve colour pairs live in `compoundVariants`; the
 * `variants` blocks carry only what a single axis can decide on its own.
 * The design draws no interactive state — this is a display element.
 */
const counterVariants = cva(
  "inline-flex shrink-0 flex-col items-center justify-center overflow-hidden rounded-full px-1 py-0.5 text-center whitespace-nowrap tabular-nums",
  {
    variants: {
      variant: {
        default: "",
        accent: "",
        red: "",
        blue: "",
        green: "",
        yellow: "",
      },
      emphasis: {
        high: "",
        low: "",
      },
      size: {
        // Height comes from padding + line-height: 2 + 16 + 2 = 20.
        default: "min-w-5 text-xs leading-4 font-medium",
        // 2 + 12 + 2 = 16.
        small: "min-w-4 text-[10px] leading-3 font-semibold",
      },
    },
    compoundVariants: [
      {
        variant: "default",
        emphasis: "high",
        className: "bg-bg-foreground text-text-inverse",
      },
      {
        variant: "accent",
        emphasis: "high",
        className: "bg-accent-default text-base-white",
      },
      {
        variant: "red",
        emphasis: "high",
        className: "bg-negative-middle text-base-white",
      },
      {
        variant: "blue",
        emphasis: "high",
        className: "bg-info-middle text-base-white",
      },
      {
        variant: "green",
        emphasis: "high",
        className: "bg-success-middle text-base-white",
      },
      {
        variant: "yellow",
        emphasis: "high",
        className: "bg-warning-middle text-base-white",
      },
      {
        variant: "default",
        emphasis: "low",
        className: "bg-bg-surface-1 text-text-strong",
      },
      {
        variant: "accent",
        emphasis: "low",
        className: "bg-teal-50 text-neutral-950",
      },
      {
        variant: "red",
        emphasis: "low",
        className: "bg-negative text-negative-middle",
      },
      {
        variant: "blue",
        emphasis: "low",
        className: "bg-info text-info-middle",
      },
      {
        variant: "green",
        emphasis: "low",
        className: "bg-success text-success-middle",
      },
      {
        variant: "yellow",
        emphasis: "low",
        className: "bg-warning text-warning-middle",
      },
    ],
    defaultVariants: {
      variant: "default",
      emphasis: "high",
      size: "default",
    },
  },
);

export type CounterProps = useRender.ComponentProps<"span"> &
  VariantProps<typeof counterVariants>;

function Counter({
  className,
  variant,
  emphasis,
  size,
  render,
  ...props
}: CounterProps) {
  return useRender({
    defaultTagName: "span",
    props: mergeProps<"span">(
      {
        className: cn(counterVariants({ variant, emphasis, size }), className),
      },
      props,
    ),
    render,
    state: {
      slot: "counter",
      variant,
      emphasis,
      size,
    },
  });
}

export { Counter, counterVariants };