Skip to main content
Urbicon UI
Back to Recipes

Stat Tile

KPI tile for dashboards: label on top, large value, optional trend indicator and icon tile on the right. Combines Card, the icon-tile pattern, and semantic intent tokens. Generic pattern for any reporting/analytics surface.

Live Preview

Compact (for sidebars / footers)

Tenants

38

Apartments

42

Occupancy

90.5%

Highlight tile (hero KPI)

Active users

1,284

+86 in the last 7 days

Features

  • Label · value · description hierarchy with tabular-nums for number alignment
  • Optional icon tile on the right with intent color (bg-{intent}-subtle / text-{intent})
  • Trend indicator with a directional arrow and intent color (up → success, down → danger)
  • Clickable via href — navigates to the detail view
  • Scales from a single tile to a grid (sm:grid-cols-2 / lg:grid-cols-4)

Code

StatTile.svelte

<script lang="ts">
  import { Card } from '@urbicon-ui/blocks';
  import type { Component } from 'svelte';

  type Intent = 'primary' | 'success' | 'warning' | 'danger' | 'neutral';

  interface Props {
    label: string;
    value: string | number;
    description?: string;
    trend?: { direction: 'up' | 'down'; label: string };
    icon?: Component;
    intent?: Intent;
    href?: string;
    onclick?: () => void;
  }

  const { label, value, description, trend, icon: Icon, intent = 'neutral', href, onclick }: Props = $props();

  const tileBg: Record<Intent, string> = {
    primary: 'bg-primary-subtle text-primary',
    success: 'bg-success-subtle text-success',
    warning: 'bg-warning-subtle text-warning-emphasis',
    danger:  'bg-danger-subtle text-danger',
    neutral: 'bg-surface-subtle text-text-secondary'
  };
</script>

<Card variant="outlined" padding="md" {href} {onclick}>
  <div class="flex items-start justify-between gap-3">
    <div class="min-w-0">
      <p class="text-text-tertiary text-xs font-medium">{label}</p>
      <p class="text-text-primary mt-2 text-2xl font-semibold tabular-nums">{value}</p>
      {#if description || trend}
        <div class="mt-1 flex items-center gap-2">
          {#if description}
            <p class="text-text-tertiary text-xs">{description}</p>
          {/if}
          {#if trend}
            <span class={`text-xs font-medium tabular-nums ${trend.direction === 'up' ? 'text-success' : 'text-danger'}`}>
              {trend.label}
            </span>
          {/if}
        </div>
      {/if}
    </div>
    {#if Icon}
      <span class={`grid h-9 w-9 shrink-0 place-items-center rounded-lg ${tileBg[intent]}`}>
        <Icon class="h-4 w-4" />
      </span>
    {/if}
  </div>
</Card>

Best Practices

tabular-nums for number alignment

tabular-nums renders digits at equal width — important when tiles sit in a grid and values should align visually.

Icon tile as a secondary element

The icon is visual labeling, not content. Hence placed on the right, smaller than the value. The intent's subtle background sets it apart visually without dominating the value.

Trend always with a comparison period

"+12.4%" without context says nothing. The description line should make clear what is being compared ("vs. last month", "vs. last year") — otherwise leave it out.

href for drill-down

Stat tiles are a classic entry point into detail views. Set href so Card renders as an <a> — hover/focus styles apply automatically, Cmd-click opens a new tab.