Skip to content
Nizivo UI

Discount card

A discount code, what it gives, its status, how much of it is used as a Meter and its dates, including an expired state that keeps its contrast. A server composition; it ships no JavaScript.

From Figma 05 Commerce, Discount Card (7275:14130; variants Active 7275:14131, Scheduled 7275:14144, Expired 7275:14157). Uses Card, Badge, Meter, Text and NumberValue, with the Tag icon from @nizivo/icons.

Examples

Discounts

Figma 05's Discount Card (7275:14130): Active, Scheduled and Expired, plus a code with no usage limit, which Figma does not draw. Usage is a Meter read as "310 of 500 used". The expired card keeps full-contrast text: only its status, its code and its meter turn neutral. A server composition: it ships no JavaScript.

  • AUTUMN20Active

    20% off all orders

    Usage

    Ends Nov 30, 2026

  • FREESHIPScheduled

    Free shipping over $50

    Usage

    Starts Nov 1, 2026

  • SUMMER15Expired

    15% off summer linen

    Usage

    Ended Sep 30, 2026

  • WELCOME10Active

    10% off a first order

    1,284 used · no limit

    Ends Dec 31, 2026

import type { JSX } from 'react';
import { Tag } from '@nizivo/icons';
import { Badge, type BadgeTone } from '@nizivo/react/badge';
import { Card } from '@nizivo/react/card';
import { Meter } from '@nizivo/react/meter';
import { NumberValue } from '@nizivo/react/number-value';
import { Text } from '@nizivo/react/text';

type Status = 'active' | 'scheduled' | 'expired';

/** Each status's words and tone. The words carry the meaning; the tone only repeats it. */
const STATUS: Record<Status, { label: string; tone: BadgeTone }> = {
  active: { label: 'Active', tone: 'success' },
  scheduled: { label: 'Scheduled', tone: 'info' },
  expired: { label: 'Expired', tone: 'neutral' },
};

// An explicit locale and time zone, so the server and the browser print the same text.
const DAY = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  day: 'numeric',
  year: 'numeric',
  timeZone: 'UTC',
});

const COUNT = new Intl.NumberFormat('en-US');

interface Discount {
  code: string;
  title: string;
  status: Status;
  used: number;
  /** The most uses allowed; `null` for no limit. */
  limit: number | null;
  /** A UTC instant: when it starts (scheduled) or when it ends (active, expired). */
  date: string;
}

const DISCOUNTS: Discount[] = [
  {
    code: 'AUTUMN20',
    title: '20% off all orders',
    status: 'active',
    used: 310,
    limit: 500,
    date: '2026-11-30T23:59:00Z',
  },
  {
    code: 'FREESHIP',
    title: 'Free shipping over $50',
    status: 'scheduled',
    used: 0,
    limit: 1000,
    date: '2026-11-01T00:00:00Z',
  },
  {
    code: 'SUMMER15',
    title: '15% off summer linen',
    status: 'expired',
    used: 500,
    limit: 500,
    date: '2026-09-30T23:59:00Z',
  },
  {
    code: 'WELCOME10',
    title: '10% off a first order',
    status: 'active',
    used: 1284,
    limit: null,
    date: '2026-12-31T23:59:00Z',
  },
];

/** The schedule line: when the discount starts, ends or ended. */
function schedule(discount: Discount): string {
  const day = DAY.format(new Date(discount.date));
  if (discount.status === 'scheduled') return `Starts ${day}`;
  return discount.status === 'expired' ? `Ended ${day}` : `Ends ${day}`;
}

/** Usage: a Meter against the limit, read as "310 of 500 used"; with no limit, a count. */
function Usage({ discount }: { discount: Discount }): JSX.Element {
  if (discount.limit === null) {
    return (
      <Text tone="secondary">
        <NumberValue value={discount.used} /> used · no limit
      </Text>
    );
  }
  return (
    <Meter
      label="Usage"
      value={discount.used}
      max={discount.limit}
      tone={discount.status === 'expired' ? 'neutral' : 'brand'}
      valueText={`${COUNT.format(discount.used)} of ${COUNT.format(discount.limit)} used`}
    />
  );
}

/** One discount: its code, what it gives, its status, how much of it is used and its dates. */
function DiscountCard({ discount }: { discount: Discount }): JSX.Element {
  const status = STATUS[discount.status];
  return (
    <Card.Root
      className="nzp-discount-card"
      render={<article aria-label={`${discount.code}, ${discount.title}`} />}
    >
      <div className="nzp-discount-card-top">
        <Text
          render={<span />}
          variant="mono-m-strong"
          className={
            discount.status === 'expired'
              ? 'nzp-discount-card-code nzp-discount-card-code-expired'
              : 'nzp-discount-card-code'
          }
        >
          <Tag size={16} />
          {discount.code}
        </Text>
        <Badge tone={status.tone}>{status.label}</Badge>
      </div>
      <Card.Title>{discount.title}</Card.Title>
      <Usage discount={discount} />
      <Text tone="secondary">{schedule(discount)}</Text>
    </Card.Root>
  );
}

export function DiscountCardExample(): JSX.Element {
  return (
    <ul className="nzp-discount-card-grid" aria-label="Discounts">
      {DISCOUNTS.map((discount) => (
        <li key={discount.code}>
          <DiscountCard discount={discount} />
        </li>
      ))}
    </ul>
  );
}

Stylesheet

discount-card.css
@layer nz.tokens, nz.themes, nz.reset, nz.base, nz.components, nz.patterns, nz.utilities;

@layer nz.patterns {
  .nzp-discount-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
    gap: var(--nz-space-4);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .nzp-discount-card {
    gap: var(--nz-space-4);
    block-size: 100%;
  }

  .nzp-discount-card-top {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--nz-space-2);
  }

  /* the code: Figma's dashed chip, in Mono */
  .nzp-discount-card-code {
    display: inline-flex;
    align-items: center;
    gap: var(--nz-space-2);
    padding: var(--nz-space-1) var(--nz-space-2);
    border: var(--nz-border-width-default) dashed var(--nz-border-brand);
    border-radius: var(--nz-radius-sm);
    background: var(--nz-bg-brand-subtle);
    color: var(--nz-text-brand);
  }

  /* expired: the code turns neutral; the text keeps its contrast (no opacity) */
  .nzp-discount-card-code-expired {
    border-color: var(--nz-border-strong);
    background: var(--nz-bg-subtle);
    color: var(--nz-text-secondary);
  }
}

Notes

  • Usage is a Meter. Figma draws a raw track; here it is a native <meter> named "Usage", whose valueText is shown and read as "310 of 500 used" instead of a bare percentage. Expired's data is corrected too: Figma's fill is 252 px in a 250 px track.
  • No limit. Figma has no unlimited code. A meter needs a maximum, so the fourth card says its count in words ("1,284 used · no limit") instead.
  • Expired keeps its contrast. Figma dims the whole Expired card with opacity: 0.70, which takes text/secondary to 3.25:1 and text/tertiary to 2.57:1. Here only the status Badge, the code and the meter turn neutral, and the words say "Expired" and "Ended"; the text stays at full contrast.
  • Dates. Scheduled gets its start and the others their end, which Figma leaves out, through Intl.DateTimeFormat with an explicit time zone.
  • The code looks copyable but is plain text. A copy button with a "Copied" announcement is a client island; add one beside the code in your app if you need it.
  • i18n. The counts go through Intl.NumberFormat; {used} of {limit} used is one message for a translation, never concatenated numbers.

On this page