# PeriodSelector A selector component for choosing time periods in charts. ## Import ```tsx import { PeriodSelector } from '@coinbase/cds-web-visualization' ``` ## Examples ### Basic Example ```jsx live 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 ( ); } ``` ### Minimum Width You can set the `width` prop to `fit-content` to make the period selector as small as possible. ```jsx live function MinimumWidthExample() { const tabs = [ { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, { id: 'YTD', label: 'YTD' }, ]; const [activeTab, setActiveTab] = useState(tabs[0]); return ( ); } ``` ### Many Periods with Overflow ```jsx live 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 ( ); } ``` ### Live Indicator ```jsx live function LiveExample() { const tabs = useMemo( () => [ // LiveTabLabel is exported from PeriodSelector { id: '1H', label: }, { 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 ( ); } ``` ### Customization #### Custom Colors ```jsx live 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 ( ); } ``` #### Color Shifting ```jsx live function ColorShiftingExample() { const TabLabel = memo(({ label }) => ( {label} )); const tabs = useMemo( () => [ { id: '1H', label: , }, { id: '1D', label: , }, { id: '1W', label: , }, { id: '1M', label: , }, { id: '1Y', label: , }, { id: 'All', label: , }, ], [], ); 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 ( ); } ``` #### 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. ```jsx live 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 }) => ( )); 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 formatYAxisPrice = useCallback((price: number) => { if (breakpoints.isPhone) { // Compact format for mobile: $45k, $1.2M, etc. if (price >= 1000000) { return `$${(price / 1000000).toFixed(1)}M`; } else if (price >= 1000) { return `$${(price / 1000).toFixed(0)}k`; } return `$${price.toFixed(0)}`; } return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(price); }, [breakpoints.isPhone]); 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 ( Asset Price} balance={} end={isMobile ? undefined : ( ) } /> {isMobile && ( )} {showSettings && ( setShowSettings(false)}> {({ handleClose }) => ( Show Y-Axis Show X-Axis )} )} ); }, []); return ; } ``` ## Props | Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `activeTab` | `TabValue \| null` | Yes | `-` | React state for the currently active tab. Setting it to null results in no active tab. | | `onChange` | `(activeTab: TabValue \| null) => void` | Yes | `-` | Callback that is fired when the active tab changes. Use this callback to update the activeTab state. | | `tabs` | `(TabValue & { Component?: TabComponent \| undefined; })[]` | Yes | `-` | The array of tabs data. Each tab may optionally define a custom Component to render. | | `TabComponent` | `TabComponent` | No | `-` | The default Component to render each tab. | | `TabsActiveIndicatorComponent` | `TabsActiveIndicatorComponent` | No | `-` | The default Component to render the tabs active indicator. | | `activeBackground` | `currentColor \| fg \| fgMuted \| fgInverse \| fgPrimary \| fgWarning \| fgPositive \| fgNegative \| bg \| bgAlternate \| bgInverse \| bgOverlay \| bgElevation1 \| bgElevation2 \| bgPrimary \| bgPrimaryWash \| bgSecondary \| bgTertiary \| bgSecondaryWash \| bgNegative \| bgNegativeWash \| bgPositive \| bgPositiveWash \| bgWarning \| bgWarningWash \| bgLine \| bgLineHeavy \| bgLineInverse \| bgLinePrimary \| bgLinePrimarySubtle \| accentSubtleRed \| accentBoldRed \| accentSubtleGreen \| accentBoldGreen \| accentSubtleBlue \| accentBoldBlue \| accentSubtlePurple \| accentBoldPurple \| accentSubtleYellow \| accentBoldYellow \| accentSubtleGray \| accentBoldGray \| transparent` | No | `-` | Background color passed to the TabsActiveIndicatorComponent. | | `alignContent` | `ResponsiveProp
` | No | `-` | - | | `alignItems` | `ResponsiveProp
` | No | `-` | - | | `alignSelf` | `ResponsiveProp
` | No | `-` | - | | `as` | `div` | No | `-` | The underlying element or component the polymorphic component will render. Changing as also changes the inherited native props (e.g. href for as=a) and the expected ref type. | | `aspectRatio` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `background` | `currentColor \| fg \| fgMuted \| fgInverse \| fgPrimary \| fgWarning \| fgPositive \| fgNegative \| bg \| bgAlternate \| bgInverse \| bgOverlay \| bgElevation1 \| bgElevation2 \| bgPrimary \| bgPrimaryWash \| bgSecondary \| bgTertiary \| bgSecondaryWash \| bgNegative \| bgNegativeWash \| bgPositive \| bgPositiveWash \| bgWarning \| bgWarningWash \| bgLine \| bgLineHeavy \| bgLineInverse \| bgLinePrimary \| bgLinePrimarySubtle \| accentSubtleRed \| accentBoldRed \| accentSubtleGreen \| accentBoldGreen \| accentSubtleBlue \| accentBoldBlue \| accentSubtlePurple \| accentBoldPurple \| accentSubtleYellow \| accentBoldYellow \| accentSubtleGray \| accentBoldGray \| transparent` | No | `-` | - | | `borderBottomLeftRadius` | `0 \| 100 \| 200 \| 300 \| 400 \| 500 \| 600 \| 700 \| 800 \| 900 \| 1000` | No | `-` | - | | `borderBottomRightRadius` | `0 \| 100 \| 200 \| 300 \| 400 \| 500 \| 600 \| 700 \| 800 \| 900 \| 1000` | No | `-` | - | | `borderBottomWidth` | `0 \| 100 \| 200 \| 300 \| 400 \| 500` | No | `-` | - | | `borderColor` | `currentColor \| fg \| fgMuted \| fgInverse \| fgPrimary \| fgWarning \| fgPositive \| fgNegative \| bg \| bgAlternate \| bgInverse \| bgOverlay \| bgElevation1 \| bgElevation2 \| bgPrimary \| bgPrimaryWash \| bgSecondary \| bgTertiary \| bgSecondaryWash \| bgNegative \| bgNegativeWash \| bgPositive \| bgPositiveWash \| bgWarning \| bgWarningWash \| bgLine \| bgLineHeavy \| bgLineInverse \| bgLinePrimary \| bgLinePrimarySubtle \| accentSubtleRed \| accentBoldRed \| accentSubtleGreen \| accentBoldGreen \| accentSubtleBlue \| accentBoldBlue \| accentSubtlePurple \| accentBoldPurple \| accentSubtleYellow \| accentBoldYellow \| accentSubtleGray \| accentBoldGray \| transparent` | No | `-` | - | | `borderEndWidth` | `0 \| 100 \| 200 \| 300 \| 400 \| 500` | No | `-` | - | | `borderRadius` | `0 \| 100 \| 200 \| 300 \| 400 \| 500 \| 600 \| 700 \| 800 \| 900 \| 1000` | No | `-` | - | | `borderStartWidth` | `0 \| 100 \| 200 \| 300 \| 400 \| 500` | No | `-` | - | | `borderTopLeftRadius` | `0 \| 100 \| 200 \| 300 \| 400 \| 500 \| 600 \| 700 \| 800 \| 900 \| 1000` | No | `-` | - | | `borderTopRightRadius` | `0 \| 100 \| 200 \| 300 \| 400 \| 500 \| 600 \| 700 \| 800 \| 900 \| 1000` | No | `-` | - | | `borderTopWidth` | `0 \| 100 \| 200 \| 300 \| 400 \| 500` | No | `-` | - | | `borderWidth` | `0 \| 100 \| 200 \| 300 \| 400 \| 500` | No | `-` | - | | `bordered` | `boolean` | No | `-` | Add a border around all sides of the box. | | `borderedBottom` | `boolean` | No | `-` | Add a border to the bottom side of the box. | | `borderedEnd` | `boolean` | No | `-` | Add a border to the trailing side of the box. | | `borderedHorizontal` | `boolean` | No | `-` | Add a border to the leading and trailing sides of the box. | | `borderedStart` | `boolean` | No | `-` | Add a border to the leading side of the box. | | `borderedTop` | `boolean` | No | `-` | Add a border to the top side of the box. | | `borderedVertical` | `boolean` | No | `-` | Add a border to the top and bottom sides of the box. | | `bottom` | `ResponsiveProp>` | No | `-` | - | | `color` | `currentColor \| fg \| fgMuted \| fgInverse \| fgPrimary \| fgWarning \| fgPositive \| fgNegative \| bg \| bgAlternate \| bgInverse \| bgOverlay \| bgElevation1 \| bgElevation2 \| bgPrimary \| bgPrimaryWash \| bgSecondary \| bgTertiary \| bgSecondaryWash \| bgNegative \| bgNegativeWash \| bgPositive \| bgPositiveWash \| bgWarning \| bgWarningWash \| bgLine \| bgLineHeavy \| bgLineInverse \| bgLinePrimary \| bgLinePrimarySubtle \| accentSubtleRed \| accentBoldRed \| accentSubtleGreen \| accentBoldGreen \| accentSubtleBlue \| accentBoldBlue \| accentSubtlePurple \| accentBoldPurple \| accentSubtleYellow \| accentBoldYellow \| accentSubtleGray \| accentBoldGray \| transparent` | No | `-` | - | | `columnGap` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `dangerouslySetBackground` | `string` | No | `-` | - | | `disabled` | `boolean` | No | `-` | Disable interactions on all the tabs. | | `display` | `ResponsiveProp` | No | `-` | - | | `elevation` | `0 \| 1 \| 2` | No | `-` | - | | `flexBasis` | `ResponsiveProp>` | No | `-` | - | | `flexDirection` | `ResponsiveProp` | No | `-` | - | | `flexGrow` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset` | No | `-` | - | | `flexShrink` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset` | No | `-` | - | | `flexWrap` | `ResponsiveProp` | No | `-` | - | | `font` | `ResponsiveProp` | No | `-` | - | | `fontFamily` | `ResponsiveProp` | No | `-` | - | | `fontSize` | `ResponsiveProp` | No | `-` | - | | `fontWeight` | `ResponsiveProp` | No | `-` | - | | `gap` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `grid` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| none` | No | `-` | - | | `gridArea` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `gridAutoColumns` | `ResponsiveProp>` | No | `-` | - | | `gridAutoFlow` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| row \| column \| dense` | No | `-` | - | | `gridAutoRows` | `ResponsiveProp>` | No | `-` | - | | `gridColumn` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `gridColumnEnd` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `gridColumnStart` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `gridRow` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `gridRowEnd` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `gridRowStart` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - | | `gridTemplate` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| none` | No | `-` | - | | `gridTemplateAreas` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| none` | No | `-` | - | | `gridTemplateColumns` | `ResponsiveProp>` | No | `-` | - | | `gridTemplateRows` | `ResponsiveProp>` | No | `-` | - | | `height` | `ResponsiveProp>` | No | `-` | - | | `justifyContent` | `ResponsiveProp` | No | `-` | - | | `key` | `Key \| null` | No | `-` | - | | `left` | `ResponsiveProp>` | No | `-` | - | | `lineHeight` | `ResponsiveProp` | No | `-` | - | | `margin` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - | | `marginBottom` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - | | `marginEnd` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - | | `marginStart` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - | | `marginTop` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - | | `marginX` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - | | `marginY` | `ResponsiveProp<0 \| -5 \| -10 \| -0.25 \| -0.5 \| -0.75 \| -1 \| -1.5 \| -2 \| -3 \| -4 \| -6 \| -7 \| -8 \| -9>` | No | `-` | - | | `maxHeight` | `ResponsiveProp>` | No | `-` | - | | `maxWidth` | `ResponsiveProp>` | No | `-` | - | | `minHeight` | `ResponsiveProp>` | No | `-` | - | | `minWidth` | `ResponsiveProp>` | No | `-` | - | | `onActiveTabElementChange` | `((element: HTMLElement \| null) => void)` | No | `-` | Optional callback to receive the active tab element. | | `opacity` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset` | No | `-` | - | | `overflow` | `ResponsiveProp` | No | `-` | - | | `padding` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `paddingBottom` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `paddingEnd` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `paddingStart` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `paddingTop` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `paddingX` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `paddingY` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `pin` | `top \| bottom \| left \| right \| all` | No | `-` | Direction in which to absolutely pin the box. | | `position` | `ResponsiveProp` | No | `-` | - | | `ref` | `((instance: HTMLElement \| null) => void) \| RefObject \| null` | No | `-` | - | | `right` | `ResponsiveProp>` | No | `-` | - | | `rowGap` | `0 \| 5 \| 10 \| 0.25 \| 0.5 \| 0.75 \| 1 \| 1.5 \| 2 \| 3 \| 4 \| 6 \| 7 \| 8 \| 9` | No | `-` | - | | `style` | `CSSProperties` | No | `-` | - | | `testID` | `string` | No | `-` | Used to locate this element in unit and end-to-end tests. Under the hood, testID translates to data-testid on Web. On Mobile, testID stays the same - testID | | `textAlign` | `ResponsiveProp
` | No | `-` | - | | `textDecoration` | `ResponsiveProp` | No | `-` | - | | `textTransform` | `ResponsiveProp` | No | `-` | - | | `top` | `ResponsiveProp>` | No | `-` | - | | `transform` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| none` | No | `-` | - | | `userSelect` | `ResponsiveProp` | No | `-` | - | | `visibility` | `ResponsiveProp` | No | `-` | - | | `width` | `ResponsiveProp>` | No | `-` | - | | `zIndex` | `-moz-initial \| inherit \| initial \| revert \| revert-layer \| unset \| auto` | No | `-` | - |