Skip to main content
PeriodSelector
@coinbase/cds-web-visualization@3.4.0-beta.1
Import
import { PeriodSelector } from '@coinbase/cds-web-visualization'
SourceView source code
Related components

Basic Example

Loading...
Live Code
function BasicExample() {
  const tabs = [
    { id: '1H', label: '1H' },
    { id: '1D', label: '1D' },
    { id: '1W', label: '1W' },
    { id: '1M', label: '1M' },
    { id: '1Y', label: '1Y' },
    { id: 'YTD', label: 'YTD' },
    { id: 'All', label: 'All' },
  ];

  const [activeTab, setActiveTab] = useState(tabs[0]);

  return <PeriodSelector activeTab={activeTab} onChange={setActiveTab} tabs={tabs} />;
}

Minimum Width

You can set the width prop to fit-content to make the period selector as small as possible.

Loading...
Live Code
function MinimumWidthExample() {
  const tabs = [
    { id: '1W', label: '1W' },
    { id: '1M', label: '1M' },
    { id: 'YTD', label: 'YTD' },
  ];

  const [activeTab, setActiveTab] = useState(tabs[0]);

  return (
    <PeriodSelector
      activeTab={activeTab}
      onChange={setActiveTab}
      tabs={tabs}
      width="fit-content"
      gap={2}
    />
  );
}

Many Periods with Overflow

Loading...
Live Code
function ManyPeriodsExample() {
  const tabs = useMemo(
    () => [
      { id: '1H', label: '1H' },
      { id: '1D', label: '1D' },
      { id: '1W', label: '1W' },
      { id: '1M', label: '1M' },
      { id: 'YTD', label: 'YTD' },
      { id: '1Y', label: '1Y' },
      { id: '5Y', label: '5Y' },
      { id: 'All', label: 'All' },
    ],
    [],
  );

  const [activeTab, setActiveTab] = useState(tabs[0]);
  const isLive = useMemo(() => activeTab?.id === '1H', [activeTab]);

  return (
    <HStack
      alignItems="center"
      justifyContent="space-between"
      maxWidth="100%"
      overflow="hidden"
      width="100%"
    >
      <Box flexGrow={1} overflow="hidden" position="relative">
        <style>{`
          .scrollContainer {
            scrollbar-width: none;
            overflow-x: auto;

            &::-webkit-scrollbar {
              display: none;
            }
          }
        `}</style>
        <Box className="scrollContainer" paddingEnd={2}>
          <PeriodSelector
            activeTab={activeTab}
            gap={1}
            justifyContent="flex-start"
            onChange={setActiveTab}
            tabs={tabs}
            width="fit-content"
          />
        </Box>
        <Box
          position="absolute"
          style={{
            background: 'linear-gradient(to left, var(--color-bg), transparent 100%)',
            right: 0,
            bottom: 0,
            top: 0,
            width: 'var(--space-4)',
            pointerEvents: 'none',
          }}
        />
      </Box>
      <IconButton
        compact
        accessibilityLabel="Configure chart"
        flexShrink={0}
        height={36}
        name="filter"
        variant="secondary"
      />
    </HStack>
  );
}

Live Indicator

Loading...
Live Code
function LiveExample() {
  const tabs = useMemo(
    () => [
      // LiveTabLabel is exported from PeriodSelector
      { id: '1H', label: <LiveTabLabel /> },
      { id: '1D', label: '1D' },
      { id: '1W', label: '1W' },
      { id: '1M', label: '1M' },
      { id: '1Y', label: '1Y' },
      { id: 'All', label: 'All' },
    ],
    [],
  );

  const [activeTab, setActiveTab] = useState(tabs[0]);
  const isLive = useMemo(() => activeTab?.id === '1H', [activeTab]);

  const activeBackground = useMemo(() => (isLive ? 'bgNegativeWash' : 'bgPrimaryWash'), [isLive]);

  return (
    <PeriodSelector
      activeBackground={activeBackground}
      activeTab={activeTab}
      onChange={setActiveTab}
      tabs={tabs}
    />
  );
}

Customization

Custom Colors

Loading...
Live Code
function LiveExample() {
  const tabs = useMemo(
    () => [
      { id: '1H', label: '1H' },
      { id: '1D', label: '1D' },
      { id: '1W', label: '1W' },
      { id: '1M', label: '1M' },
      { id: '1Y', label: '1Y' },
      { id: 'All', label: 'All' },
    ],
    [],
  );

  const [activeTab, setActiveTab] = useState(tabs[0]);
  const isLive = useMemo(() => activeTab?.id === 'live', [activeTab]);

  const activeBackground = useMemo(() => (isLive ? 'bgNegativeWash' : 'bgPrimaryWash'), [isLive]);

  return (
    <PeriodSelector
      activeBackground={activeBackground}
      activeTab={activeTab}
      onChange={setActiveTab}
      tabs={tabs}
    />
  );
}

Color Shifting

Loading...
Live Code
function ColorShiftingExample() {
  const TabLabel = memo(({ label }) => (
    <Text font="label1" style={{ color: 'var(--chartActiveColor)' }}>
      {label}
    </Text>
  ));

  const tabs = useMemo(
    () => [
      {
        id: '1H',
        label: <TabLabel label="1H" />,
      },
      {
        id: '1D',
        label: <TabLabel label="1D" />,
      },
      {
        id: '1W',
        label: <TabLabel label="1W" />,
      },
      {
        id: '1M',
        label: <TabLabel label="1M" />,
      },
      {
        id: '1Y',
        label: <TabLabel label="1Y" />,
      },
      {
        id: 'All',
        label: <TabLabel label="All" />,
      },
    ],
    [],
  );

  const [activeTab, setActiveTab] = useState(tabs[0]);
  const [chartActiveColor, setChartActiveColor] = useState('positive');

  const toggleColor = useCallback(() => {
    setChartActiveColor((activeColor) => (activeColor === 'positive' ? 'negative' : 'positive'));
  }, []);

  const activeForegroundColor = useMemo(() => {
    return chartActiveColor === 'positive' ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)';
  }, [chartActiveColor]);

  const activeBackground = useMemo(() => {
    return chartActiveColor === 'positive' ? 'bgPositiveWash' : 'bgNegativeWash';
  }, [chartActiveColor]);

  return (
    <VStack gap={2}>
      <m.div
        animate={{ '--chartActiveColor': activeForegroundColor }}
        style={{ '--chartActiveColor': activeForegroundColor }}
        transition={{ duration: 0.3 }}
      >
        <PeriodSelector
          activeBackground={activeBackground}
          activeTab={activeTab}
          onChange={setActiveTab}
          tabs={tabs}
        />
      </m.div>
      <Button onClick={toggleColor}>Toggle Color</Button>
    </VStack>
  );
}

Within Section Header

Loading...
Live Code
function SectionHeaderExample() {
  const tabs = [
    { id: '1D', label: '1D' },
    { id: '1W', label: '1W' },
    { id: '1M', label: '1M' },
  ];

  const [activeTab, setActiveTab] = useState(tabs[0]);
  return (
    <SectionHeader
      end={
        <HStack alignItems="center">
          <PeriodSelector
            activeTab={activeTab}
            onChange={setActiveTab}
            tabs={tabs}
            width="fit-content"
          />
        </HStack>
      }
      title={<Text font="label1">Portfolio Balance</Text>}
      balance={
        <Text font="display3" color="fgMuted">
          $10,023.82
        </Text>
      }
    />
  );
}

Asset Price Chart

You can use a PeriodSelector to control the time period of a LineChart, with a settings icon to enable extra customization for users.

Loading...
Live Code
function CustomizableAssetPriceExample() {
  const tabs = [
    { id: 'hour', label: '1H' },
    { id: 'day', label: '1D' },
    { id: 'week', label: '1W' },
    { id: 'month', label: '1M' },
    { id: 'year', label: '1Y' },
    { id: 'all', label: 'All' },
  ];

  const PeriodSelectorWrapper = memo(({ activeTab, setActiveTab, tabs, onClickSettings }) => (
    <HStack
      alignItems="center"
      justifyContent="space-between"
      maxWidth="100%"
      overflow="hidden"
      width="100%"
    >
      <Box flexGrow={1} overflow="hidden" position="relative">
        <style>{`
          .scrollContainer {
            scrollbar-width: none;
            overflow-x: auto;

            &::-webkit-scrollbar {
              display: none;
            }
          }
        `}</style>
        <Box className="scrollContainer" paddingEnd={2}>
          <PeriodSelector
            activeTab={activeTab}
            gap={1}
            justifyContent="flex-start"
            onChange={setActiveTab}
            tabs={tabs}
            width="fit-content"
          />
        </Box>
        <Box
          position="absolute"
          style={{
            background: 'linear-gradient(to left, var(--color-bg), transparent 100%)',
            right: 0,
            bottom: 0,
            top: 0,
            width: 'var(--space-4)',
            pointerEvents: 'none',
          }}
        />
      </Box>
      <IconButton
        compact
        accessibilityLabel="Chart settings"
        flexShrink={0}
        height={36}
        name="settings"
        variant="secondary"
        onClick={onClickSettings}
      />
    </HStack>
  ));

  const AssetPriceChart = memo(() => {
    const [activeTab, setActiveTab] = useState(tabs[0]);
    const [showSettings, setShowSettings] = useState(false);
    const [showYAxis, setShowYAxis] = useState(true);
    const [showXAxis, setShowXAxis] = useState(true);
    const [scrubIndex, setScrubIndex] = useState();
    const breakpoints = useBreakpoints();


    const formatPrice = useCallback((price: number) => {
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      }).format(price);
    }, []);
    const toggleShowYAxis = useCallback(() => setShowYAxis((show) => !show), []);
    const toggleShowXAxis = useCallback(() => setShowXAxis((show) => !show), []);

    const data = useMemo(() => sparklineInteractiveData[activeTab.id], [activeTab.id]);
    const currentPrice = useMemo(() => sparklineInteractiveData.hour[sparklineInteractiveData.hour.length - 1].value, []);
    const currentTimePrice = useMemo(() => {
      if (scrubIndex !== undefined) {
        return data[scrubIndex].value;
      }
      return currentPrice;
    }, [data, scrubIndex, currentPrice]);

    const formatDate = useCallback((date) => {
      const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'short' });
      const monthDay = date.toLocaleDateString('en-US', {
        month: 'short',
        day: 'numeric',
      });
      const time = date.toLocaleTimeString('en-US', {
        hour: 'numeric',
        minute: '2-digit',
        hour12: true,
      });
      return `${dayOfWeek}, ${monthDay}, ${time}`;
    }, []);

    const scrubberLabel = useMemo(() => {
      if (scrubIndex === undefined) return;
      return formatDate(data[scrubIndex].date);
    }, [scrubIndex, data, formatDate]);

    const accessibilityLabel = useMemo(() => {
      if (scrubIndex === undefined) return;
      const price = new Intl.NumberFormat('en-US', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      }).format(data[scrubIndex].value);
      const date = formatDate(data[scrubIndex].date);
      return `Asset price: ${price} USD on ${date}`;
    }, [scrubIndex, data, formatDate]);

    const onClickSettings = useCallback(() => setShowSettings(!showSettings), [showSettings]);

    const seriesData = useMemo(() => [{ id: 'price', data: data.map((d) => d.value) }], [data]);

    const getFormattingConfigForPeriod = useCallback((period) => {
      switch (period) {
        case 'hour':
        case 'day':
          return {
            hour: 'numeric',
            minute: 'numeric',
          };

        case 'week':
        case 'month':
          return {
            month: 'numeric',
            day: 'numeric',
          };

        case 'year':
        case 'all':
          return {
            month: 'numeric',
            year: 'numeric',
          };
      }
    }, []);

    const formatXAxisDate = useCallback((index) => {
      if (!data[index]) return '';
      const date = data[index].date;
      const formatConfig = getFormattingConfigForPeriod(activeTab.id);

      if (activeTab.id === 'hour' || activeTab.id === 'day') {
        return date.toLocaleTimeString('en-US', formatConfig);
      } else {
        return date.toLocaleDateString('en-US', formatConfig);
      }
    }, [data, activeTab.id, getFormattingConfigForPeriod]);

    const isMobile = breakpoints.isPhone || breakpoints.isTabletPortrait;

    return (
      <VStack gap={2}>
        <SectionHeader
          padding={0}
          title={<Text font="label1">Asset Price</Text>}
          balance={<RollingNumber format={{ style: 'currency', currency: 'USD' }} font="display3" color="fgMuted" value={currentTimePrice} />}
          end={isMobile ? undefined : (
            <HStack alignItems="center">
              <PeriodSelectorWrapper
                activeTab={activeTab}
                setActiveTab={setActiveTab}
                tabs={tabs}
                onClickSettings={onClickSettings}
              />
            </HStack>)
          }
        />
        <LineChart
          enableScrubbing
          height={300}
          onScrubberPositionChange={setScrubIndex}
          series={seriesData}
          yAxis={{
            domainLimit: 'strict',
            showGrid: true,
            tickLabelFormatter: formatPrice,
            width: 80
          }}
          xAxis={{
            tickLabelFormatter: formatXAxisDate,
          }}
          showYAxis={showYAxis}
          showXAxis={showXAxis}
          accessibilityLabel={accessibilityLabel}
        >
          <Scrubber label={scrubberLabel} />
        </LineChart>
        {isMobile && (
          <HStack alignItems="center">
            <PeriodSelectorWrapper
              activeTab={activeTab}
              setActiveTab={setActiveTab}
              tabs={tabs}
              onClickSettings={onClickSettings}
            />
          </HStack>
        )}
        {showSettings && (
          <Tray title="Chart Settings" onCloseComplete={() => setShowSettings(false)}>
            {({ handleClose }) => (
              <VStack gap={2} paddingX={3} paddingBottom={3}>
                <HStack justifyContent="space-between" alignItems="center">
                  <Text font="label1">Show Y-Axis</Text>
                  <Switch checked={showYAxis} onChange={toggleShowYAxis} />
                </HStack>

                <HStack justifyContent="space-between" alignItems="center">
                  <Text font="label1">Show X-Axis</Text>
                  <Switch checked={showXAxis} onChange={toggleShowXAxis} />
                </HStack>
              </VStack>
            )}
          </Tray>
        )}
      </VStack>
    );
  }, []);

  return <AssetPriceChart />;
}

Is this page useful?

Coinbase Design is an open-source, adaptable system of guidelines, components, and tools that aid the best practices of user interface design for crypto products.