Chart Range Control

Shared accessible range and period control for BoardCN chart cards.

Control

The shared range control chart cards use.

function ChartRangeControlDemo() {
  return <ChartRangeControl ranges={RANGES} value="7d" onValueChange={() => undefined} />;
}
function ChartRangeControlDemo() {
  return <ChartRangeControl ranges={RANGES} value="7d" onValueChange={() => undefined} />;
}

Installation

npx shadcn@latest add @boardcn/chart-range-control
npx shadcn@latest add @boardcn/chart-range-control

npm packages

  • @remixicon/react

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

Source

The file the CLI copies into your project.

components/charts/chart-range-control.tsx
"use client";

import { useState } from "react";
import { RiArrowDownSLine, RiCalendarLine, RiCheckLine } from "@remixicon/react";
import {
  Dropdown,
  DropdownItem,
  DropdownPopover,
  DropdownTrigger,
} from "@/components/base/dropdown/dropdown";
import { cx } from "@/utils/cx";

export interface ChartRangeOption {
  id: string;
  label: string;
}

export interface ChartRangeControlProps<T extends ChartRangeOption> {
  /** Available periods. With no periods, `label` is rendered as a static pill. */
  ranges?: readonly T[];
  value?: string;
  onValueChange?: (id: string) => void;
  label?: string;
  className?: string;
  /** Area charts use BoardCN's narrower, truncating date-range treatment. */
  variant?: "default" | "compact";
}

const STATIC_CLASS =
  "flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default pr-2.5 pl-2 shadow-xs";
const TRIGGER_CLASS =
  "flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default pr-1.5 pl-2 shadow-xs transition-colors duration-150 ease-out hover:bg-background-primary-hover";
const COMPACT_STATIC_CLASS =
  "inline-flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-2 text-body-medium text-text-primary shadow-xs";
const COMPACT_TRIGGER_CLASS =
  "inline-flex h-8 max-w-[164px] shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-2 text-body-medium text-text-primary shadow-xs";

/**
 * Shared BoardCN chart period control. The dropdown implementation owns open
 * state so the chevron, dismissal, and close-on-select behavior cannot drift
 * between chart cards. Keyboard focus and activation are provided by the
 * shared Dropdown primitives.
 */
export function ChartRangeControl<T extends ChartRangeOption>({
  ranges,
  value,
  onValueChange,
  label,
  className,
  variant = "default",
}: ChartRangeControlProps<T>) {
  const [open, setOpen] = useState(false);
  const selected = ranges?.find((item) => item.id === value) ?? ranges?.[0];
  const displayLabel = selected?.label ?? label;
  const compact = variant === "compact";

  if (!ranges?.length) {
    if (!displayLabel) return null;

    const Tag = compact ? "span" : "div";
    return (
      <Tag className={cx(compact ? COMPACT_STATIC_CLASS : STATIC_CLASS, className)}>
        <RiCalendarLine
          className={cx(
            "size-4 shrink-0",
            compact ? "text-foreground-icon-secondary" : "text-text-secondary",
          )}
          aria-hidden
        />
        <span className={compact ? "truncate" : "text-body-medium whitespace-nowrap text-text-primary"}>
          {displayLabel}
        </span>
      </Tag>
    );
  }

  return (
    <Dropdown isOpen={open} onOpenChange={setOpen}>
      <DropdownTrigger
        aria-label={compact ? `Date range: ${displayLabel}` : "Change period"}
        className={cx(compact ? COMPACT_TRIGGER_CLASS : TRIGGER_CLASS, className)}
      >
        <RiCalendarLine
          className={cx(
            "size-4 shrink-0",
            compact ? "text-foreground-icon-secondary" : "text-text-secondary",
          )}
          aria-hidden
        />
        <span className={compact ? "truncate" : "text-body-medium whitespace-nowrap text-text-primary"}>
          {displayLabel}
        </span>
        <RiArrowDownSLine
          data-range-chevron
          className={cx(
            "size-4 shrink-0 transition-transform duration-200 ease-out motion-reduce:transition-none",
            !compact && "text-text-secondary",
            open && "rotate-180",
          )}
          aria-hidden
        />
      </DropdownTrigger>
      <DropdownPopover
        aria-label={compact ? "Select date range" : "Period"}
        placement={compact ? undefined : "bottom end"}
        className={compact ? "w-[188px]" : "w-[188px] p-2"}
        dialogClassName={compact ? undefined : "gap-1"}
      >
        {ranges.map((item) => {
          const isSelected = item.id === selected?.id;
          return (
            <DropdownItem
              key={item.id}
              selected={isSelected}
              className={cx("px-2 py-1.5", compact && "justify-between text-body-medium")}
              onSelect={() => {
                onValueChange?.(item.id);
                setOpen(false);
              }}
            >
              <span className={compact ? undefined : "flex-1 text-body-medium"}>{item.label}</span>
              {isSelected && (
                <RiCheckLine
                  className={cx("size-4 shrink-0", !compact && "text-text-secondary")}
                  aria-hidden
                />
              )}
            </DropdownItem>
          );
        })}
      </DropdownPopover>
    </Dropdown>
  );
}
"use client";

import { useState } from "react";
import { RiArrowDownSLine, RiCalendarLine, RiCheckLine } from "@remixicon/react";
import {
  Dropdown,
  DropdownItem,
  DropdownPopover,
  DropdownTrigger,
} from "@/components/base/dropdown/dropdown";
import { cx } from "@/utils/cx";

export interface ChartRangeOption {
  id: string;
  label: string;
}

export interface ChartRangeControlProps<T extends ChartRangeOption> {
  /** Available periods. With no periods, `label` is rendered as a static pill. */
  ranges?: readonly T[];
  value?: string;
  onValueChange?: (id: string) => void;
  label?: string;
  className?: string;
  /** Area charts use BoardCN's narrower, truncating date-range treatment. */
  variant?: "default" | "compact";
}

const STATIC_CLASS =
  "flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default pr-2.5 pl-2 shadow-xs";
const TRIGGER_CLASS =
  "flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default pr-1.5 pl-2 shadow-xs transition-colors duration-150 ease-out hover:bg-background-primary-hover";
const COMPACT_STATIC_CLASS =
  "inline-flex h-8 shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-2 text-body-medium text-text-primary shadow-xs";
const COMPACT_TRIGGER_CLASS =
  "inline-flex h-8 max-w-[164px] shrink-0 items-center gap-1.5 rounded-2lg border border-border-button-default bg-background-primary-default px-2 text-body-medium text-text-primary shadow-xs";

/**
 * Shared BoardCN chart period control. The dropdown implementation owns open
 * state so the chevron, dismissal, and close-on-select behavior cannot drift
 * between chart cards. Keyboard focus and activation are provided by the
 * shared Dropdown primitives.
 */
export function ChartRangeControl<T extends ChartRangeOption>({
  ranges,
  value,
  onValueChange,
  label,
  className,
  variant = "default",
}: ChartRangeControlProps<T>) {
  const [open, setOpen] = useState(false);
  const selected = ranges?.find((item) => item.id === value) ?? ranges?.[0];
  const displayLabel = selected?.label ?? label;
  const compact = variant === "compact";

  if (!ranges?.length) {
    if (!displayLabel) return null;

    const Tag = compact ? "span" : "div";
    return (
      <Tag className={cx(compact ? COMPACT_STATIC_CLASS : STATIC_CLASS, className)}>
        <RiCalendarLine
          className={cx(
            "size-4 shrink-0",
            compact ? "text-foreground-icon-secondary" : "text-text-secondary",
          )}
          aria-hidden
        />
        <span className={compact ? "truncate" : "text-body-medium whitespace-nowrap text-text-primary"}>
          {displayLabel}
        </span>
      </Tag>
    );
  }

  return (
    <Dropdown isOpen={open} onOpenChange={setOpen}>
      <DropdownTrigger
        aria-label={compact ? `Date range: ${displayLabel}` : "Change period"}
        className={cx(compact ? COMPACT_TRIGGER_CLASS : TRIGGER_CLASS, className)}
      >
        <RiCalendarLine
          className={cx(
            "size-4 shrink-0",
            compact ? "text-foreground-icon-secondary" : "text-text-secondary",
          )}
          aria-hidden
        />
        <span className={compact ? "truncate" : "text-body-medium whitespace-nowrap text-text-primary"}>
          {displayLabel}
        </span>
        <RiArrowDownSLine
          data-range-chevron
          className={cx(
            "size-4 shrink-0 transition-transform duration-200 ease-out motion-reduce:transition-none",
            !compact && "text-text-secondary",
            open && "rotate-180",
          )}
          aria-hidden
        />
      </DropdownTrigger>
      <DropdownPopover
        aria-label={compact ? "Select date range" : "Period"}
        placement={compact ? undefined : "bottom end"}
        className={compact ? "w-[188px]" : "w-[188px] p-2"}
        dialogClassName={compact ? undefined : "gap-1"}
      >
        {ranges.map((item) => {
          const isSelected = item.id === selected?.id;
          return (
            <DropdownItem
              key={item.id}
              selected={isSelected}
              className={cx("px-2 py-1.5", compact && "justify-between text-body-medium")}
              onSelect={() => {
                onValueChange?.(item.id);
                setOpen(false);
              }}
            >
              <span className={compact ? undefined : "flex-1 text-body-medium"}>{item.label}</span>
              {isSelected && (
                <RiCheckLine
                  className={cx("size-4 shrink-0", !compact && "text-text-secondary")}
                  aria-hidden
                />
              )}
            </DropdownItem>
          );
        })}
      </DropdownPopover>
    </Dropdown>
  );
}

Props

Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.

ChartRangeControl

Shared BoardCN chart period control. The dropdown implementation owns open state so the chevron, dismissal, and close-on-select behavior cannot drift between chart cards. Keyboard focus and activation are provided by the shared Dropdown primitives.

PropTypeDefaultDescription
classNamestring
labelstring
onValueChange(id: string) => void
rangesreadonly T[]Available periods. With no periods, `label` is rendered as a static pill.
valuestring
variant"default" | "compact"defaultArea charts use BoardCN's narrower, truncating date-range treatment.