# PeriodSelector **📖 Live documentation:** https://cds.coinbase.com/components/charts/PeriodSelector/ A selector component for choosing time periods in charts. ## Import ```tsx import { PeriodSelector } from '@coinbase/cds-web-visualization' ``` ## Examples PeriodSelector is a specialized [SegmentedTabs](/components/navigation/SegmentedTabs) optimized for chart time-period selection. It provides a transparent background, primary wash active state, and full-width layout by default. ### Basics ```jsx live function Example() { 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 ( ); } ``` ### Sizing Set `width` to `fit-content` to make the selector only as wide as its content, and use `gap` to control spacing between tabs. ```jsx live function Example() { const tabs = [ { id: '1W', label: '1W' }, { id: '1M', label: '1M' }, { id: 'YTD', label: 'YTD' }, ]; const [activeTab, setActiveTab] = useState(tabs[0]); return ( ); } ``` ### Live Indicator Use the `LiveTabLabel` component (exported from PeriodSelector) to indicate a live data period. Pair it with a conditional `activeBackground` to visually differentiate the live state. ```jsx live function Example() { const tabs = useMemo( () => [ { 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 ( ); } ``` ### Overflow When there are too many tabs to fit in a single row, wrap the selector in a scrollable container with a fade edge and an optional action button. ```jsx live function Example() { 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]); return ( ); } ``` ### Customization #### Custom Colors Use the `activeBackground` prop to change the active indicator color. This example conditionally applies a negative wash when the live period is selected. ```jsx live function Example() { const tabs = useMemo( () => [ { 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[1]); const isLive = useMemo(() => activeTab?.id === '1H', [activeTab]); const activeBackground = useMemo(() => (isLive ? 'bgNegativeWash' : 'bgPrimaryWash'), [isLive]); return ( ); } ``` #### Color Shifting Animate the active tab's foreground color using a CSS variable and framer-motion. This pattern is useful for charts where the color changes based on price movement (positive/negative). ```jsx live function Example() { 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 A composed example using PeriodSelector to control the time period of a [LineChart](/components/charts/LineChart), with a settings tray for axis toggles. ```jsx live function Example() { 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) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(price); }, []); const formatYAxisPrice = useCallback( (price) => { if (breakpoints.isPhone) { 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` | `Color` | 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` | `ResponsiveProp` | No | `-` | - | | `background` | `ResponsiveProp` | No | `-` | - | | `borderBottomLeftRadius` | `ResponsiveProp` | No | `-` | - | | `borderBottomRightRadius` | `ResponsiveProp` | No | `-` | - | | `borderBottomWidth` | `ResponsiveProp` | No | `-` | - | | `borderColor` | `ResponsiveProp` | No | `-` | - | | `borderEndWidth` | `ResponsiveProp` | No | `-` | - | | `borderRadius` | `ResponsiveProp` | No | `-` | - | | `borderStartWidth` | `ResponsiveProp` | No | `-` | - | | `borderTopLeftRadius` | `ResponsiveProp` | No | `-` | - | | `borderTopRightRadius` | `ResponsiveProp` | No | `-` | - | | `borderTopWidth` | `ResponsiveProp` | No | `-` | - | | `borderWidth` | `ResponsiveProp` | 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 | `-` | - | | `classNames` | `{ root?: string; tab?: string \| undefined; activeIndicator?: string \| undefined; } \| undefined` | No | `-` | Custom class names for individual elements of the SegmentedTabs component | | `color` | `ResponsiveProp` | No | `-` | - | | `columnGap` | `ResponsiveProp` | No | `-` | - | | `dangerouslySetBackground` | `string` | No | `-` | - | | `disabled` | `boolean` | No | `-` | Disable interactions on all the tabs. | | `display` | `ResponsiveProp` | No | `-` | - | | `elevation` | `ResponsiveProp` | No | `-` | - | | `flexBasis` | `ResponsiveProp>` | No | `-` | - | | `flexDirection` | `ResponsiveProp` | No | `-` | - | | `flexGrow` | `ResponsiveProp` | No | `-` | - | | `flexShrink` | `ResponsiveProp` | No | `-` | - | | `flexWrap` | `ResponsiveProp` | No | `-` | - | | `font` | `ResponsiveProp` | No | `-` | - | | `fontFamily` | `ResponsiveProp` | No | `-` | - | | `fontSize` | `ResponsiveProp` | No | `-` | - | | `fontWeight` | `ResponsiveProp` | No | `-` | - | | `gap` | `ResponsiveProp` | No | `-` | - | | `grid` | `ResponsiveProp` | No | `-` | - | | `gridArea` | `ResponsiveProp` | No | `-` | - | | `gridAutoColumns` | `ResponsiveProp>` | No | `-` | - | | `gridAutoFlow` | `ResponsiveProp` | No | `-` | - | | `gridAutoRows` | `ResponsiveProp>` | No | `-` | - | | `gridColumn` | `ResponsiveProp` | No | `-` | - | | `gridColumnEnd` | `ResponsiveProp` | No | `-` | - | | `gridColumnStart` | `ResponsiveProp` | No | `-` | - | | `gridRow` | `ResponsiveProp` | No | `-` | - | | `gridRowEnd` | `ResponsiveProp` | No | `-` | - | | `gridRowStart` | `ResponsiveProp` | No | `-` | - | | `gridTemplate` | `ResponsiveProp` | No | `-` | - | | `gridTemplateAreas` | `ResponsiveProp` | 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 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - | | `marginBottom` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - | | `marginEnd` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - | | `marginStart` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - | | `marginTop` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - | | `marginX` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - | | `marginY` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | 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` | `ResponsiveProp` | No | `-` | - | | `overflow` | `ResponsiveProp` | No | `-` | - | | `padding` | `ResponsiveProp` | No | `-` | - | | `paddingBottom` | `ResponsiveProp` | No | `-` | - | | `paddingEnd` | `ResponsiveProp` | No | `-` | - | | `paddingStart` | `ResponsiveProp` | No | `-` | - | | `paddingTop` | `ResponsiveProp` | No | `-` | - | | `paddingX` | `ResponsiveProp` | No | `-` | - | | `paddingY` | `ResponsiveProp` | No | `-` | - | | `pin` | `PinningDirection` | No | `-` | Direction in which to absolutely pin the box. | | `position` | `ResponsiveProp` | No | `-` | - | | `ref` | `((instance: HTMLElement \| null) => void) \| RefObject \| null` | No | `-` | - | | `right` | `ResponsiveProp>` | No | `-` | - | | `rowGap` | `ResponsiveProp` | No | `-` | - | | `style` | `CSSProperties` | No | `-` | - | | `styles` | `{ root?: CSSProperties; tab?: CSSProperties \| undefined; activeIndicator?: CSSProperties \| undefined; } \| undefined` | No | `-` | Custom styles for individual elements of the SegmentedTabs component | | `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` | `ResponsiveProp` | No | `-` | - | | `userSelect` | `ResponsiveProp` | No | `-` | - | | `visibility` | `ResponsiveProp` | No | `-` | - | | `width` | `ResponsiveProp>` | No | `-` | - | | `zIndex` | `ResponsiveProp` | No | `-` | - | ## Styles | Selector | Static class name | Description | | --- | --- | --- | | `root` | `-` | Root element | | `tab` | `-` | Tab element | | `activeIndicator` | `-` | Active indicator element |