KPI Card Set

Show key metrics, trend indicators, and status highlights in a reusable KPI card group for dashboards.

Custom CodeUI Design & LayoutsYeeflow
custom codekpi cardsmetricsdashboardtsx

What this template helps teams build

Overview

The KPI Card Set template is a reusable custom code control concept for Yeeflow builders. It helps teams add a focused interface pattern for KPI monitoring and dashboard summaries while keeping the asset in review before public launch.

Key capabilities

  • Supports dashboard & analytics scenarios
  • Designed for use in Dashboard, Application page
  • Includes TSX source, user guidance, and example configuration
  • Prepared as a preview asset for technical review and future public documentation

Recommended use cases

  • KPI monitoring and dashboard summaries
  • Dashboard & Analytics template library planning
  • Internal builder education and implementation review

Governance notes

This record is created in review status for editorial and product validation. It should not be positioned as launchable until implementation, security, runtime, and documentation review are complete.

Build patterns behind this template

Use this template as a reference while reviewing the Custom Code developer guide. Learn how the required export structure, input parameters, and rendering patterns fit together.

Code preview

Download source file
import React from 'react';

type KpiCard = {
  title: string;
  value: any;
  subtitle: string;
  trend: string;
  trendDirection: string;
  tone: string;
  prefix: string;
  suffix: string;
  format: string;
  decimals?: number;
  icon: string;
  targetLabel: string;
  targetValue: string;
};

export class CodeInApplication implements CodeInComp {
  description() {
    return 'KPI Card Set - reusable Yeeflow dashboard custom code template for displaying configurable KPI cards.';
  }

  requiredFields(params?: CodeInParams) {
    return [];
  }

  inputParameters(): InputParameter[] {
    return [
      {
        id: 'cardsConfig',
        name: 'Cards Config',
        type: 'variable',
        desc: 'Expression-editor value containing KPI card configuration. Supports JSON array, object with cards/items/data, variable, temp variable, or expression result.',
      },
      {
        id: 'titleText',
        name: 'Title Text',
        type: 'string',
        desc: 'Optional title shown above the KPI card set.',
      },
      {
        id: 'subtitleText',
        name: 'Subtitle Text',
        type: 'string',
        desc: 'Optional supporting text shown below the title.',
      },
      {
        id: 'layoutMode',
        name: 'Layout Mode',
        type: 'string',
        desc: 'Card layout: auto, one, two, three, or four. Default is auto.',
      },
      {
        id: 'cardTone',
        name: 'Default Card Tone',
        type: 'string',
        desc: 'Default tone for cards without their own tone: blue, green, amber, red, violet, or slate.',
      },
      {
        id: 'titleField',
        name: 'Title Field',
        type: 'variable',
        desc: 'Optional source data field used as each KPI card title. Example: MetricName.',
      },
      {
        id: 'valueField',
        name: 'Value Field',
        type: 'variable',
        desc: 'Optional source data field used as each KPI card value. Example: MetricValue.',
      },
      {
        id: 'subtitleField',
        name: 'Subtitle Field',
        type: 'variable',
        desc: 'Optional source data field used as each KPI card subtitle/helper text.',
      },
      {
        id: 'trendField',
        name: 'Trend Field',
        type: 'variable',
        desc: 'Optional source data field used as each KPI card trend/change text.',
      },
      {
        id: 'trendDirectionField',
        name: 'Trend Direction Field',
        type: 'variable',
        desc: 'Optional source data field used as trend direction: up, down, or neutral.',
      },
      {
        id: 'toneField',
        name: 'Tone Field',
        type: 'variable',
        desc: 'Optional source data field used as card tone: blue, green, amber, red, violet, or slate.',
      },
      {
        id: 'formatField',
        name: 'Format Field',
        type: 'variable',
        desc: 'Optional source data field used as value format: auto, number, percent, or text.',
      },
      {
        id: 'prefixField',
        name: 'Prefix Field',
        type: 'variable',
        desc: 'Optional source data field used as text before the KPI value.',
      },
      {
        id: 'suffixField',
        name: 'Suffix Field',
        type: 'variable',
        desc: 'Optional source data field used as text after the KPI value.',
      },
      {
        id: 'iconField',
        name: 'Icon Field',
        type: 'variable',
        desc: 'Optional source data field used as the small card marker/icon text.',
      },
      {
        id: 'targetLabelField',
        name: 'Target Label Field',
        type: 'variable',
        desc: 'Optional source data field used as the target label.',
      },
      {
        id: 'targetValueField',
        name: 'Target Value Field',
        type: 'variable',
        desc: 'Optional source data field used as the target value.',
      },
      {
        id: 'decimalsField',
        name: 'Decimals Field',
        type: 'variable',
        desc: 'Optional source data field used as card-specific decimal places.',
      },
      {
        id: 'showTrend',
        name: 'Show Trend',
        type: 'variable',
        desc: 'Whether trend/change text should be shown. Supports true/false or dynamic expression.',
      },
      {
        id: 'showIcon',
        name: 'Show Icon',
        type: 'variable',
        desc: 'Whether card icons/initial markers should be shown. Supports true/false or dynamic expression.',
      },
      {
        id: 'compactMode',
        name: 'Compact Mode',
        type: 'variable',
        desc: 'Whether compact spacing is enabled. Supports true/false or dynamic expression.',
      },
      {
        id: 'emptyStateText',
        name: 'Empty State Text',
        type: 'string',
        desc: 'Text shown when no KPI card configuration is available.',
      },
      {
        id: 'numberLocale',
        name: 'Number Locale',
        type: 'string',
        desc: 'Locale used for number formatting, such as en-US. Leave blank for browser default.',
      },
      {
        id: 'decimalPlaces',
        name: 'Decimal Places',
        type: 'string',
        desc: 'Default decimal places for numeric KPI values. Default is 0.',
      },
      {
        id: 'minCardWidth',
        name: 'Minimum Card Width',
        type: 'string',
        desc: 'Minimum card width in pixels when layoutMode is auto. Default is 220.',
      },
      {
        id: 'valueSize',
        name: 'Value Size',
        type: 'string',
        desc: 'Value text size: small, medium, or large. Default is medium.',
      },
    ];
  }

  render(context: CodeInContext, fieldsValues: any, readonly: boolean) {
    return (
      <KpiCardSet
        context={context}
        fieldsValues={fieldsValues}
        readonly={readonly}
        params={(context && context.params) || {}}
      />
    );
  }
}

class KpiCardSet extends React.Component<any, any> {
  getConfig() {
    const params = this.props.params || {};
    const defaultTone = this.normalizeTone(this.normalizeToString(params.cardTone) || 'blue');
    const fieldMap = {
      title: this.normalizeToString(params.titleField),
      value: this.normalizeToString(params.valueField),
      subtitle: this.normalizeToString(params.subtitleField),
      trend: this.normalizeToString(params.trendField),
      trendDirection: this.normalizeToString(params.trendDirectionField),
      tone: this.normalizeToString(params.toneField),
      format: this.normalizeToString(params.formatField),
      prefix: this.normalizeToString(params.prefixField),
      suffix: this.normalizeToString(params.suffixField),
      icon: this.normalizeToString(params.iconField),
      targetLabel: this.normalizeToString(params.targetLabelField),
      targetValue: this.normalizeToString(params.targetValueField),
      decimals: this.normalizeToString(params.decimalsField),
    };
    return {
      cards: this.parseCardsConfig(params.cardsConfig, defaultTone, fieldMap),
      titleText: this.normalizeToString(params.titleText),
      subtitleText: this.normalizeToString(params.subtitleText),
      layoutMode: this.normalizeLayoutMode(this.normalizeToString(params.layoutMode)),
      cardTone: defaultTone,
      fieldMap: fieldMap,
      showTrend: this.normalizeToBoolean(params.showTrend, true),
      showIcon: this.normalizeToBoolean(params.showIcon, true),
      compactMode: this.normalizeToBoolean(params.compactMode, false),
      emptyStateText: this.normalizeToString(params.emptyStateText) || 'No KPI cards configured',
      numberLocale: this.normalizeToString(params.numberLocale),
      decimalPlaces: this.normalizeToNumber(params.decimalPlaces, 0),
      minCardWidth: Math.max(160, this.normalizeToNumber(params.minCardWidth, 220)),
      valueSize: this.normalizeValueSize(this.normalizeToString(params.valueSize)),
    };
  }

  parseCardsConfig(rawValue: any, defaultTone: string, fieldMap: any): KpiCard[] {
    const resolved = this.resolveParameterValue(rawValue);
    const parsed = this.safeParseJson(resolved);
    const source = parsed !== null && parsed !== undefined ? parsed : resolved;
    const cardSource = this.extractCardArray(source);
    return cardSource
      .map((item: any, index: number) => this.normalizeCard(item, index, defaultTone, fieldMap))
      .filter((card: KpiCard | null) => Boolean(card)) as KpiCard[];
  }

  extractCardArray(value: any): any[] {
    if (Array.isArray(value)) {
      return value;
    }
    if (value && typeof value === 'object') {
      const candidates = [
        value.cards,
        value.Cards,
        value.items,
        value.Items,
        value.data,
        value.Data,
        value.list,
        value.List,
        value.values,
        value.Values,
      ];
      for (let index = 0; index < candidates.length; index += 1) {
        if (Array.isArray(candidates[index])) {
          return candidates[index];
        }
      }
      if (value.title !== undefined || value.label !== undefined || value.value !== undefined) {
        return [value];
      }
    }
    const text = this.normalizeToString(value);
    if (!text) {
      return [];
    }
    /*
      Fallback for simple manual configuration:
      Each line can be "Title | Value | Subtitle | Trend | Tone".
      JSON is recommended for production use because it is clearer and safer.
    */
    return text
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean)
      .map((line) => line.split('|').map((part) => part.trim()));
  }

  normalizeCard(item: any, index: number, defaultTone: string, fieldMap: any): KpiCard | null {
    if (Array.isArray(item)) {
      const title = this.normalizeToString(item[0]);
      const value = item.length > 1 ? item[1] : '';
      if (!title && this.normalizeToString(value) === '') {
        return null;
      }
      return {
        title: title || 'KPI ' + (index + 1),
        value: value,
        subtitle: this.normalizeToString(item[2]),
        trend: this.normalizeToString(item[3]),
        trendDirection: this.inferTrendDirection(item[3]),
        tone: this.normalizeTone(this.normalizeToString(item[4]) || defaultTone),
        prefix: '',
        suffix: '',
        format: 'auto',
        icon: '',
        targetLabel: '',
        targetValue: '',
      };
    }

    if (item && typeof item === 'object') {
      const title = this.normalizeToString(
        this.firstDefined([
          this.readMappedField(item, fieldMap.title),
          item.title,
          item.label,
          item.name,
          item.heading,
        ])
      );
      const value = this.firstDefined([
        this.readMappedField(item, fieldMap.value),
        item.value,
        item.count,
        item.number,
        item.total,
        item.amount,
      ]);
      if (!title && this.normalizeToString(value) === '') {
        return null;
      }
      const trend = this.normalizeToString(
        this.firstDefined([
          this.readMappedField(item, fieldMap.trend),
          item.trend,
          item.trendValue,
          item.change,
          item.delta,
        ])
      );
      const trendDirection = this.normalizeToString(
        this.firstDefined([
          this.readMappedField(item, fieldMap.trendDirection),
          item.trendDirection,
          item.direction,
          item.status,
        ])
      ) || this.inferTrendDirection(trend);
      const decimalsValue = this.firstDefined([
        this.readMappedField(item, fieldMap.decimals),
        item.decimals,
      ]);
      return {
        title: title || 'KPI ' + (index + 1),
        value: value,
        subtitle: this.normalizeToString(
          this.firstDefined([
            this.readMappedField(item, fieldMap.subtitle),
            item.subtitle,
            item.helper,
            item.description,
            item.caption,
          ])
        ),
        trend: trend,
        trendDirection: this.normalizeTrendDirection(trendDirection),
        tone: this.normalizeTone(this.normalizeToString(this.firstDefined([
          this.readMappedField(item, fieldMap.tone),
          item.tone,
          item.color,
          item.accent,
        ])) || defaultTone),
        prefix: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.prefix), item.prefix])),
        suffix: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.suffix), item.suffix])),
        format: this.normalizeFormat(this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.format), item.format]))),
        decimals: decimalsValue !== undefined ? this.normalizeToNumber(decimalsValue, 0) : undefined,
        icon: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.icon), item.icon, item.initials])),
        targetLabel: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.targetLabel), item.targetLabel, item.targetName])),
        targetValue: this.normalizeToString(this.firstDefined([this.readMappedField(item, fieldMap.targetValue), item.targetValue, item.target])),
      };
    }

    const primitiveValue = this.normalizeToString(item);
    if (!primitiveValue) {
      return null;
    }
    return {
      title: 'KPI ' + (index + 1),
      value: primitiveValue,
      subtitle: '',
      trend: '',
      trendDirection: 'neutral',
      tone: defaultTone,
      prefix: '',
      suffix: '',
      format: 'auto',
      icon: '',
      targetLabel: '',
      targetValue: '',
    };
  }

  firstDefined(values: any[]) {
    for (let index = 0; index < values.length; index += 1) {
      if (values[index] !== undefined && values[index] !== null) {
        return values[index];
      }
    }
    return undefined;
  }

  readMappedField(item: any, fieldName: string): any {
    const fieldPath = this.normalizeToString(fieldName);
    if (!item || !fieldPath) {
      return undefined;
    }
    const directValue = this.readObjectValueByKey(item, fieldPath);
    if (directValue !== undefined) {
      return directValue;
    }
    if (fieldPath.indexOf('.') !== -1) {
      const parts = fieldPath.split('.');
      let current = item;
      for (let index = 0; index < parts.length; index += 1) {
        current = this.readObjectValueByKey(current, parts[index]);
        if (current === undefined || current === null) {
          return undefined;
        }
      }
      return current;
    }
    return undefined;
  }

  readObjectValueByKey(source: any, wantedKey: string): any {
    if (!source || typeof source !== 'object' || Array.isArray(source) || !wantedKey) {
      return undefined;
    }
    if (source[wantedKey] !== undefined) {
      return source[wantedKey];
    }
    const normalizedWanted = wantedKey.toLowerCase();
    const keys = Object.keys(source);
    for (let index = 0; index < keys.length; index += 1) {
      if (keys[index].toLowerCase() === normalizedWanted) {
        return source[keys[index]];
      }
    }
    return undefined;
  }

  resolveParameterValue(value: any): any {
    if (value === undefined || value === null) {
      return '';
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) {
      return value;
    }
    if (typeof value === 'object') {
      const preferred =
        value.value !== undefined ? value.value :
        value.Value !== undefined ? value.Value :
        value.data !== undefined ? value.data :
        value.Data !== undefined ? value.Data :
        value.items !== undefined ? value.items :
        value.Items !== undefined ? value.Items :
        value.cards !== undefined ? value.cards :
        value.Cards !== undefined ? value.Cards :
        value.result !== undefined ? value.result :
        value.Result !== undefined ? value.Result :
        value.key !== undefined ? value.key :
        value.Key !== undefined ? value.Key :
        value.label !== undefined ? value.label :
        value.Label !== undefined ? value.Label : undefined;
      if (preferred !== undefined) {
        return this.resolveParameterValue(preferred);
      }
    }
    return value;
  }

  safeParseJson(value: any): any {
    if (Array.isArray(value) || (value && typeof value === 'object')) {
      return value;
    }
    const text = this.normalizeToString(value);
    if (!text) {
      return null;
    }
    try {
      return JSON.parse(text);
    } catch (error) {
      return null;
    }
  }

  normalizeToString(value: any): string {
    if (value === undefined || value === null) {
      return '';
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
      return String(value).trim();
    }
    if (Array.isArray(value)) {
      return value.map((item) => this.normalizeToString(item)).filter(Boolean).join(', ');
    }
    if (typeof value === 'object') {
      const resolved =
        value.value !== undefined ? value.value :
        value.Value !== undefined ? value.Value :
        value.text !== undefined ? value.text :
        value.Text !== undefined ? value.Text :
        value.display !== undefined ? value.display :
        value.Display !== undefined ? value.Display :
        value.label !== undefined ? value.label :
        value.Label !== undefined ? value.Label :
        value.name !== undefined ? value.name :
        value.Name !== undefined ? value.Name :
        value.title !== undefined ? value.title :
        value.Title !== undefined ? value.Title :
        value.key !== undefined ? value.key :
        value.Key !== undefined ? value.Key : undefined;
      if (resolved !== undefined) {
        return this.normalizeToString(resolved);
      }
      try {
        return JSON.stringify(value);
      } catch (error) {
        return '';
      }
    }
    return String(value).trim();
  }

  normalizeToBoolean(value: any, fallback: boolean): boolean {
    if (value === true || value === false) {
      return value;
    }
    if (Array.isArray(value) && value.length > 0) {
      return this.normalizeToBoolean(value[0], fallback);
    }
    if (value && typeof value === 'object') {
      const objectValue =
        value.value !== undefined ? value.value :
        value.Value !== undefined ? value.Value :
        value.checked !== undefined ? value.checked :
        value.Checked !== undefined ? value.Checked :
        value.selected !== undefined ? value.selected :
        value.Selected !== undefined ? value.Selected :
        value.key !== undefined ? value.key :
        value.Key !== undefined ? value.Key :
        value.label !== undefined ? value.label :
        value.Label !== undefined ? value.Label : undefined;
      if (objectValue !== undefined) {
        return this.normalizeToBoolean(objectValue, fallback);
      }
    }
    const text = this.normalizeToString(value).toLowerCase();
    if (['true', '1', 'yes', 'y', 'on', 'show', 'enabled'].indexOf(text) !== -1) {
      return true;
    }
    if (['false', '0', 'no', 'n', 'off', 'hide', 'disabled'].indexOf(text) !== -1) {
      return false;
    }
    return fallback;
  }

  normalizeToNumber(value: any, fallback: number): number {
    if (typeof value === 'number' && isFinite(value)) {
      return value;
    }
    const text = this.normalizeToString(value).replace(/,/g, '').replace(/%/g, '');
    if (!text) {
      return fallback;
    }
    const parsed = parseFloat(text);
    return isFinite(parsed) ? parsed : fallback;
  }

  formatValue(card: KpiCard, config: any): string {
    const rawText = this.normalizeToString(card.value);
    const format = this.normalizeFormat(card.format);
    if (format === 'text') {
      return card.prefix + rawText + card.suffix;
    }
    const numericValue = this.toNumberOrNull(card.value);
    if (numericValue === null || format === 'auto' && !this.looksNumeric(rawText)) {
      return card.prefix + rawText + card.suffix;
    }
    const decimals = card.decimals !== undefined ? card.decimals : config.decimalPlaces;
    const normalizedDecimals = Math.max(0, Math.min(6, decimals));
    let formatted = '';
    try {
      formatted = numericValue.toLocaleString(config.numberLocale || undefined, {
        minimumFractionDigits: normalizedDecimals,
        maximumFractionDigits: normalizedDecimals,
      });
    } catch (error) {
      formatted = String(numericValue);
    }
    if (format === 'percent') {
      return card.prefix + formatted + '%' + card.suffix;
    }
    return card.prefix + formatted + card.suffix;
  }

  toNumberOrNull(value: any): number | null {
    if (typeof value === 'number' && isFinite(value)) {
      return value;
    }
    const text = this.normalizeToString(value).replace(/,/g, '').replace(/%/g, '');
    if (!text) {
      return null;
    }
    const parsed = parseFloat(text);
    return isFinite(parsed) ? parsed : null;
  }

  looksNumeric(value: string) {
    return /^-?\d+(\.\d+)?%?$/.test((value || '').replace(/,/g, '').trim());
  }

  normalizeLayoutMode(value: string) {
    const normalized = (value || '').toLowerCase();
    if (['one', 'two', 'three', 'four', 'auto'].indexOf(normalized) !== -1) {
      return normalized;
    }
    if (['1', '2', '3', '4'].indexOf(normalized) !== -1) {
      return ['one', 'two', 'three', 'four'][parseInt(normalized, 10) - 1];
    }
    return 'auto';
  }

  normalizeTone(value: string) {
    const normalized = (value || '').toLowerCase();
    if (['blue', 'green', 'amber', 'red', 'violet', 'slate'].indexOf(normalized) !== -1) {
      return normalized;
    }
    return 'blue';
  }

  normalizeFormat(value: string) {
    const normalized = (value || '').toLowerCase();
    if (['auto', 'number', 'text', 'percent'].indexOf(normalized) !== -1) {
      return normalized;
    }
    return 'auto';
  }

  normalizeValueSize(value: string) {
    const normalized = (value || '').toLowerCase();
    if (['small', 'medium', 'large'].indexOf(normalized) !== -1) {
      return normalized;
    }
    return 'medium';
  }

  normalizeTrendDirection(value: any) {
    const normalized = this.normalizeToString(value).toLowerCase();
    if (['up', 'positive', 'good', 'increase', 'success'].indexOf(normalized) !== -1) {
      return 'up';
    }
    if (['down', 'negative', 'bad', 'decrease', 'danger'].indexOf(normalized) !== -1) {
      return 'down';
    }
    return 'neutral';
  }

  inferTrendDirection(value: any) {
    const text = this.normalizeToString(value);
    if (text.indexOf('-') === 0) {
      return 'down';
    }
    if (text.indexOf('+') === 0) {
      return 'up';
    }
    return 'neutral';
  }

  getGridStyle(config: any) {
    const minWidth = config.minCardWidth + 'px';
    const map: any = {
      one: 'repeat(1, minmax(0, 1fr))',
      two: 'repeat(2, minmax(0, 1fr))',
      three: 'repeat(3, minmax(0, 1fr))',
      four: 'repeat(4, minmax(0, 1fr))',
      auto: 'repeat(auto-fit, minmax(' + minWidth + ', 1fr))',
    };
    return {
      gridTemplateColumns: map[config.layoutMode] || map.auto,
    };
  }

  getCardIcon(card: KpiCard) {
    const configured = this.normalizeToString(card.icon);
    if (configured) {
      return configured.substring(0, 3).toUpperCase();
    }
    const words = this.normalizeToString(card.title).split(/\s+/).filter(Boolean);
    if (words.length === 0) {
      return 'KPI';
    }
    if (words.length === 1) {
      return words[0].substring(0, 2).toUpperCase();
    }
    return (words[0].charAt(0) + words[1].charAt(0)).toUpperCase();
  }

  renderTrend(card: KpiCard, config: any) {
    if (!config.showTrend || !card.trend) {
      return null;
    }
    return (
      <span className={'kcs-trend kcs-trend-' + this.normalizeTrendDirection(card.trendDirection)}>
        {card.trend}
      </span>
    );
  }

  renderTarget(card: KpiCard) {
    if (!card.targetLabel && !card.targetValue) {
      return null;
    }
    return (
      <div className="kcs-target">
        {card.targetLabel ? <span>{card.targetLabel}</span> : null}
        {card.targetValue ? <strong>{card.targetValue}</strong> : null}
      </div>
    );
  }

  renderCard(card: KpiCard, index: number, config: any) {
    return (
      <div className={'kcs-card kcs-tone-' + card.tone} key={index}>
        <div className="kcs-card-top">
          <div>
            <div className="kcs-title">{card.title}</div>
            {card.subtitle ? <div className="kcs-subtitle">{card.subtitle}</div> : null}
          </div>
          {config.showIcon ? <div className="kcs-icon">{this.getCardIcon(card)}</div> : null}
        </div>
        <div className={'kcs-value kcs-value-' + config.valueSize}>{this.formatValue(card, config)}</div>
        <div className="kcs-card-bottom">
          {this.renderTrend(card, config)}
          {this.renderTarget(card)}
        </div>
      </div>
    );
  }

  renderEmptyState(config: any) {
    return (
      <div className="kcs-empty">
        <div className="kcs-empty-title">{config.emptyStateText}</div>
        <div className="kcs-empty-help">Configure cardsConfig with KPI card data to show dashboard metrics.</div>
      </div>
    );
  }

  render() {
    const config = this.getConfig();
    const rootClass = 'kpi-card-set' + (config.compactMode ? ' kcs-compact' : '');
    const styles = [
      '.kpi-card-set{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;color:#172033;width:100%;box-sizing:border-box;}',
      '.kcs-header{margin:0 0 14px 0;}',
      '.kcs-heading{font-size:18px;line-height:1.3;font-weight:700;color:#111827;margin:0;}',
      '.kcs-support{font-size:13px;line-height:1.45;color:#64748b;margin:4px 0 0 0;}',
      '.kcs-grid{display:grid;gap:14px;width:100%;}',
      '.kcs-card{position:relative;overflow:hidden;min-height:138px;border:1px solid #d9e2ef;border-radius:8px;background:#fff;padding:16px;box-sizing:border-box;box-shadow:0 1px 2px rgba(15,23,42,0.04);}',
      '.kcs-card:before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:#2f73ff;}',
      '.kcs-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;}',
      '.kcs-title{font-size:13px;line-height:1.35;font-weight:650;color:#344054;}',
      '.kcs-subtitle{font-size:12px;line-height:1.4;color:#7a8699;margin-top:3px;}',
      '.kcs-icon{flex:0 0 auto;min-width:34px;height:28px;border-radius:7px;background:#eef4ff;border:1px solid #cfe0ff;color:#185abc;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:750;letter-spacing:0;}',
      '.kcs-value{font-weight:750;color:#0f172a;margin-top:18px;line-height:1.05;letter-spacing:0;}',
      '.kcs-value-small{font-size:24px;}',
      '.kcs-value-medium{font-size:30px;}',
      '.kcs-value-large{font-size:38px;}',
      '.kcs-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:24px;margin-top:12px;}',
      '.kcs-trend{display:inline-flex;align-items:center;min-height:24px;border-radius:999px;padding:0 9px;font-size:12px;font-weight:700;border:1px solid transparent;}',
      '.kcs-trend-up{color:#067647;background:#ecfdf3;border-color:#abefc6;}',
      '.kcs-trend-down{color:#b42318;background:#fef3f2;border-color:#fecdca;}',
      '.kcs-trend-neutral{color:#475467;background:#f8fafc;border-color:#e4e7ec;}',
      '.kcs-target{display:flex;align-items:baseline;gap:5px;font-size:12px;color:#667085;min-width:0;}',
      '.kcs-target strong{font-size:12px;color:#344054;font-weight:700;white-space:nowrap;}',
      '.kcs-tone-blue:before{background:#2f73ff;}.kcs-tone-blue .kcs-icon{background:#eef4ff;border-color:#cfe0ff;color:#185abc;}',
      '.kcs-tone-green:before{background:#12b76a;}.kcs-tone-green .kcs-icon{background:#ecfdf3;border-color:#abefc6;color:#067647;}',
      '.kcs-tone-amber:before{background:#f79009;}.kcs-tone-amber .kcs-icon{background:#fffaeb;border-color:#fedf89;color:#b54708;}',
      '.kcs-tone-red:before{background:#f04438;}.kcs-tone-red .kcs-icon{background:#fef3f2;border-color:#fecdca;color:#b42318;}',
      '.kcs-tone-violet:before{background:#7a5af8;}.kcs-tone-violet .kcs-icon{background:#f4f3ff;border-color:#d9d6fe;color:#5925dc;}',
      '.kcs-tone-slate:before{background:#667085;}.kcs-tone-slate .kcs-icon{background:#f8fafc;border-color:#e4e7ec;color:#475467;}',
      '.kcs-empty{border:1px dashed #cbd5e1;border-radius:8px;background:#f8fafc;padding:18px;color:#64748b;}',
      '.kcs-empty-title{font-size:14px;font-weight:700;color:#344054;}',
      '.kcs-empty-help{font-size:12px;line-height:1.5;margin-top:4px;}',
      '.kcs-compact .kcs-grid{gap:10px;}',
      '.kcs-compact .kcs-card{min-height:112px;padding:12px;}',
      '.kcs-compact .kcs-value{margin-top:12px;}',
      '@media(max-width:640px){.kcs-grid{grid-template-columns:1fr!important;}.kcs-value-large{font-size:32px;}.kcs-card{min-height:auto;}.kcs-card-bottom{align-items:flex-start;flex-direction:column;}}',
    ].join('');

    return (
      <div className={rootClass}>
        <style>{styles}</style>
        {config.titleText || config.subtitleText ? (
          <div className="kcs-header">
            {config.titleText ? <div className="kcs-heading">{config.titleText}</div> : null}
            {config.subtitleText ? <div className="kcs-support">{config.subtitleText}</div> : null}
          </div>
        ) : null}
        {config.cards.length > 0 ? (
          <div className="kcs-grid" style={this.getGridStyle(config)}>
            {config.cards.map((card: KpiCard, index: number) => this.renderCard(card, index, config))}
          </div>
        ) : this.renderEmptyState(config)}
      </div>
    );
  }
}

User guide

Download guide

KPI Card Set

A. Template name

KPI Card Set

B. Short description

KPI Card Set is a reusable Yeeflow dashboard custom code template that displays a clean set of configurable KPI cards from a JSON array, variable, temp variable, or expression result.

C. Purpose / what this is for

This template helps teams present important business metrics on Yeeflow dashboards in a consistent, readable format. It is useful when a dashboard needs to show a quick operational summary, such as totals, counts, performance values, service levels, or current status indicators.

Customers use this control when they want a polished KPI section without building custom UI from scratch each time. It keeps the display logic reusable while allowing each app or dashboard to provide different metric data.

D. Supported placement

This template is optimized for:

  • Dashboard page

It can display values from fixed JSON, dashboard variables, temp variables, or expressions. It does not write values back to form fields or temp variables.

E. When to use this control

Use this control when:

  • A dashboard needs 2-8 summary metrics.
  • Metrics are already available as variables, temp variables, or expression results.
  • The same KPI card layout should be reused across multiple dashboards.
  • Delivery teams need a clean, enterprise-ready dashboard component.
  • You want consistent visual treatment for totals, trends, targets, and status indicators.

Do not use this control when the dashboard needs a full chart, pivot table, drill-down table, or complex data exploration experience.

F. Input parameters overview table
Parameter nameTypeRequiredPurposeExample value
cardsConfigExpression / variableRecommendedKPI card data as JSON array, object, variable, temp variable, or expression result.See example below
titleTextPlain textOptionalTitle shown above the KPI card set.Executive Summary
subtitleTextPlain textOptionalSupporting text below the title.Current operational performance
layoutModePlain textOptionalCard layout: auto, one, two, three, or four.auto
cardTonePlain textOptionalDefault card color tone.blue
titleFieldExpression / variableOptionalSource JSON field used as the card title.MetricName
valueFieldExpression / variableOptionalSource JSON field used as the card value.MetricValue
subtitleFieldExpression / variableOptionalSource JSON field used as subtitle/helper text.Description
trendFieldExpression / variableOptionalSource JSON field used as trend/change text.ChangeText
trendDirectionFieldExpression / variableOptionalSource JSON field used as trend direction.ChangeDirection
toneFieldExpression / variableOptionalSource JSON field used as card tone.StatusColor
formatFieldExpression / variableOptionalSource JSON field used as value format.ValueFormat
prefixFieldExpression / variableOptionalSource JSON field used as value prefix.CurrencySymbol
suffixFieldExpression / variableOptionalSource JSON field used as value suffix.UnitText
iconFieldExpression / variableOptionalSource JSON field used as icon/marker text.ShortCode
targetLabelFieldExpression / variableOptionalSource JSON field used as target label.GoalLabel
targetValueFieldExpression / variableOptionalSource JSON field used as target value.GoalValue
decimalsFieldExpression / variableOptionalSource JSON field used as decimal places.DecimalPlaces
showTrendExpression / variableOptionalShows or hides trend labels.true
showIconExpression / variableOptionalShows or hides card icon/initial markers.true
compactModeExpression / variableOptionalUses tighter spacing for dense dashboards.false
emptyStateTextPlain textOptionalText shown when no card data is configured.No KPI data available
numberLocalePlain textOptionalLocale for number formatting.en-US
decimalPlacesNumeric config textOptionalDefault decimal places for numeric values.0
minCardWidthNumeric config textOptionalMinimum card width in auto layout.220
valueSizePlain textOptionalKPI value size: small, medium, or large.medium
G. Detailed parameter explanation
cardsConfig

The main KPI data source. This parameter uses the expression editor type so it can support static JSON, dashboard variables, temp variables, complex variables, or dynamic expressions.

Recommended format:

[
  {
    "title": "Open Requests",
    "value": 42,
    "subtitle": "Pending review",
    "trend": "+12%",
    "trendDirection": "up",
    "tone": "blue",
    "format": "number"
  }
]

Supported card fields include:

FieldPurposeExample
titleCard title.Open Requests
valueMain KPI value.42
subtitleShort supporting text.Pending review
trendChange or trend label.+12%
trendDirectionTrend style: up, down, or neutral.up
toneCard tone: blue, green, amber, red, violet, slate.green
formatValue format: auto, number, percent, or text.number
prefixText before the value.$
suffixText after the value.hrs
decimalsCard-specific decimal places.1
iconSmall text marker shown in the icon area.REQ
targetLabelOptional target label.Target
targetValueOptional target value.95%

If cardsConfig is an object with cards, items, data, list, or values, the template will try to use that array. If it is a JSON string, the template will parse it safely. If it is invalid or blank, the empty state is shown.

You can use cardsConfig in two ways:

  1. Use the built-in KPI schema shown above, with fields such as title, value, subtitle, and trend.
  2. Pass any JSON array and use the field-mapping parameters below to tell the template which source fields to read.

Flexible source data example:

[
  {
    "MetricName": "Open Requests",
    "MetricValue": 42,
    "Description": "Pending review",
    "ChangeText": "+12%",
    "ChangeDirection": "up",
    "StatusColor": "blue",
    "ValueFormat": "number"
  }
]

For the example above, configure:

ParameterValue
titleFieldMetricName
valueFieldMetricValue
subtitleFieldDescription
trendFieldChangeText
trendDirectionFieldChangeDirection
toneFieldStatusColor
formatFieldValueFormat

Field names are matched case-insensitively. Nested paths are also supported using dot notation, such as metric.name or summary.currentValue.

Field mapping parameters

Field mapping parameters are optional expression-editor parameters. Use them when the data source uses customer-specific field names instead of the built-in KPI schema.

If a mapping parameter is blank, the template falls back to the built-in field names. For example, if titleField is blank, the template looks for title, label, name, or heading.

titleField

The field in each source JSON item used as the KPI title.

Example: MetricName

valueField

The field in each source JSON item used as the main KPI value.

Example: MetricValue

subtitleField

The field used as the small helper text under the title.

Example: Description

trendField

The field used as the trend or change label.

Example: ChangeText

trendDirectionField

The field used to style the trend as up, down, or neutral.

Example: ChangeDirection

toneField

The field used as each card's color tone.

Example: StatusColor

formatField

The field used as each value's display format.

Example: ValueFormat

prefixField and suffixField

Fields used to place text before or after the KPI value.

Examples: CurrencySymbol, UnitText

iconField

The field used as the small card marker text.

Example: ShortCode

targetLabelField and targetValueField

Fields used to show target or goal information at the bottom of the card.

Examples: GoalLabel, GoalValue

decimalsField

The field used as card-specific decimal places.

Example: DecimalPlaces

titleText

Optional title shown above the card grid. Use this when the KPI section needs a dashboard heading.

Example: Procurement Overview

subtitleText

Optional helper text under the title. Keep it short and business-friendly.

Example: Live summary for active supplier requests

layoutMode

Controls how many columns the card grid tries to use.

Supported values:

  • auto: responsive layout based on available width
  • one: one column
  • two: two columns
  • three: three columns
  • four: four columns

Use auto for most dashboards.

cardTone

Default visual tone for cards that do not define their own tone.

Supported values:

  • blue
  • green
  • amber
  • red
  • violet
  • slate

Use calm tones that match the business meaning. For example, green for healthy values, amber for warning, red for risk, blue for neutral operational metrics.

showTrend

Controls whether trend labels are shown. This is an expression-editor parameter, so it can be a fixed true/false, dashboard variable, temp variable, or expression.

Accepted values include true, false, 1, 0, yes, no, on, and off.

showIcon

Controls whether the small card marker is shown. If a card has an icon, that text is shown. Otherwise, the template derives initials from the card title.

This parameter supports expression-editor values.

compactMode

Uses tighter spacing and smaller card height. This is useful for dense dashboards or narrow dashboard sections.

This parameter supports expression-editor values.

emptyStateText

Text shown when no valid cards are available.

Example: No KPI data available

numberLocale

Locale used for number formatting. Leave blank to use the browser default.

Examples:

  • en-US
  • en-GB
  • de-DE
decimalPlaces

Default number of decimal places for numeric KPI values. A card can override this with its own decimals field.

Example: 1

minCardWidth

Minimum card width in pixels when layoutMode is auto.

Recommended values:

  • 200 for dense dashboards
  • 220 for normal dashboards
  • 260 for wider executive dashboards
valueSize

Controls the size of the KPI value.

Supported values:

  • small
  • medium
  • large

Use medium for most dashboards.

H. Step-by-step setup guide
  1. Add a Custom Code control to a Yeeflow dashboard page.
  2. Open the custom code editor.
  3. Paste the kpi-card-set.tsx code.
  4. Save the code.
  5. Open input parameters.
  6. Configure cardsConfig with a JSON array or select a dashboard variable/temp variable that returns the KPI card array.
  7. If the source JSON does not use the built-in KPI field names, configure titleField, valueField, subtitleField, trendField, and any other field mappings you need.
  8. Optionally configure titleText and subtitleText.
  9. Set layoutMode to auto unless a fixed column count is needed.
  10. Configure showTrend, showIcon, and compactMode as needed.
  11. Save the dashboard.
  12. Preview and confirm the card values, layout, number formatting, trend styles, and mobile/narrow behavior.
I. Result / expected output

After setup, users will see a responsive set of KPI cards on the dashboard. Each card can show:

  • title
  • main value
  • subtitle
  • trend/change label
  • tone/accent color
  • optional icon marker
  • optional target label/value

The template is display-only. It does not save values back into Yeeflow fields or variables.

J. Real business examples
Procurement dashboard

Show open purchase requests, approved requests this month, overdue supplier reviews, and average approval cycle time.

Customer service dashboard

Show open tickets, SLA compliance, average response time, and escalations.

HR operations dashboard

Show active onboarding cases, pending approvals, monthly new hires, and training completion rate.

Asset management dashboard

Show total assets, maintenance due, overdue inspections, and asset utilization.

K. Notes / assumptions / limitations
  • This template is optimized for dashboard display.
  • It expects KPI values to be prepared by variables, temp variables, expressions, or upstream dashboard logic.
  • It supports custom source JSON field names through field mapping parameters.
  • It does not query a Yeeflow data list directly in this version.
  • It does not save output values.
  • Invalid JSON or empty card data will show the empty state instead of crashing.
  • percent format appends % to the provided numeric value. If you want 85%, provide 85, not 0.85.
  • Use short card titles and subtitles for best dashboard readability.
L. Testing checklist
  • Confirm the dashboard renders with a valid cardsConfig JSON array.
  • Confirm field mappings work with customer-specific JSON field names.
  • Confirm blank field mappings fall back to the built-in KPI schema.
  • Confirm the empty state appears when cardsConfig is blank or invalid.
  • Test layoutMode: auto, two, three, and four.
  • Test showTrend with true and false.
  • Test showIcon with true and false.
  • Test compactMode with true and false.
  • Test numeric values with numberLocale and decimalPlaces.
  • Test card-level tones: blue, green, amber, red, violet, slate.
  • Test mobile or narrow dashboard width.
  • Test expression-editor input using a dashboard temp variable.
M. Troubleshooting
No cards appear

Check that cardsConfig is not blank and contains a valid JSON array or an expression result with a card array. If the data is inside an object, use a property such as cards, items, or data.

The value formatting looks wrong

Check each card's format, prefix, suffix, and decimals. For percent values, pass the display number directly, such as 92, and use "format": "percent".

Trend color is not correct

Set trendDirection on the card to up, down, or neutral. If this is omitted, the template infers up from values starting with + and down from values starting with -.

The layout has too many or too few columns

Use layoutMode for fixed layouts or minCardWidth to tune responsive auto layout.

A variable or temp variable does not render as expected

Check what the variable returns. The template supports arrays, JSON strings, and objects with cards, items, or data, but the final resolved value must contain card data.

Example configuration

Download config

KPI Card Set Example Configuration

Use either example in the cardsConfig parameter for a quick dashboard preview.

Example A: Built-in KPI schema

This example works without field mapping parameters because it uses the template's built-in field names.

[
  {
    "title": "Open Requests",
    "value": 42,
    "subtitle": "Pending review",
    "trend": "+12%",
    "trendDirection": "up",
    "tone": "blue",
    "format": "number",
    "icon": "REQ",
    "targetLabel": "Target",
    "targetValue": "< 50"
  },
  {
    "title": "Approval Rate",
    "value": 91.4,
    "subtitle": "This month",
    "trend": "+4.2%",
    "trendDirection": "up",
    "tone": "green",
    "format": "percent",
    "decimals": 1,
    "icon": "APR",
    "targetLabel": "Goal",
    "targetValue": "90%"
  },
  {
    "title": "Overdue Items",
    "value": 7,
    "subtitle": "Need attention",
    "trend": "-3",
    "trendDirection": "down",
    "tone": "amber",
    "format": "number",
    "icon": "OD"
  },
  {
    "title": "Cycle Time",
    "value": 3.8,
    "subtitle": "Average business days",
    "trend": "Stable",
    "trendDirection": "neutral",
    "tone": "slate",
    "format": "number",
    "decimals": 1,
    "suffix": " days",
    "icon": "CT"
  }
]

Recommended parameter values:

ParameterValue
titleTextOperational KPI Summary
subtitleTextLive dashboard metrics for the current reporting period
layoutModeauto
cardToneblue
showTrendtrue
showIcontrue
compactModefalse
numberLocaleen-US
decimalPlaces0
minCardWidth220
valueSizemedium
Example B: Custom source JSON with field mappings

This example is useful when the source data comes from an API, dashboard variable, temp variable, AI result, or data transform that uses different field names.

cardsConfig:

[
  {
    "MetricName": "Open Requests",
    "MetricValue": 42,
    "MetricDescription": "Pending review",
    "ChangeText": "+12%",
    "ChangeDirection": "up",
    "StatusColor": "blue",
    "ValueFormat": "number",
    "Marker": "REQ",
    "GoalLabel": "Target",
    "GoalValue": "< 50"
  },
  {
    "MetricName": "Approval Rate",
    "MetricValue": 91.4,
    "MetricDescription": "This month",
    "ChangeText": "+4.2%",
    "ChangeDirection": "up",
    "StatusColor": "green",
    "ValueFormat": "percent",
    "DecimalPlaces": 1,
    "Marker": "APR",
    "GoalLabel": "Goal",
    "GoalValue": "90%"
  },
  {
    "MetricName": "Overdue Items",
    "MetricValue": 7,
    "MetricDescription": "Need attention",
    "ChangeText": "-3",
    "ChangeDirection": "down",
    "StatusColor": "amber",
    "ValueFormat": "number",
    "Marker": "OD"
  },
  {
    "MetricName": "Cycle Time",
    "MetricValue": 3.8,
    "MetricDescription": "Average business days",
    "ChangeText": "Stable",
    "ChangeDirection": "neutral",
    "StatusColor": "slate",
    "ValueFormat": "number",
    "DecimalPlaces": 1,
    "UnitText": " days",
    "Marker": "CT"
  }
]

Field mapping parameter values:

ParameterValue
titleFieldMetricName
valueFieldMetricValue
subtitleFieldMetricDescription
trendFieldChangeText
trendDirectionFieldChangeDirection
toneFieldStatusColor
formatFieldValueFormat
suffixFieldUnitText
iconFieldMarker
targetLabelFieldGoalLabel
targetValueFieldGoalValue
decimalsFieldDecimalPlaces