It's quite a common UI pattern to add a delimiter between elements, such as a dot or a slash, to separate pieces of text or create divisions between sections on a page. Today, I would like to share abstract React components that are built on top of basic flexbox CSS, allowing you to implement such use cases in seconds.
Let's start with the StackSeparatedBy
component and its horizontal version, HStackSeparatedBy
. These components take a separator element and render it between their children. They are built on top of the Stack
component, which I have described in detail in this post. By using Children.toArray
, we can remove null
children. However, it's important to note that if one of the children is a component that returns null
, we won't be able to recognize it.
import React, { Fragment, ReactNode } from "react"
import { Stack, StackProps } from "./Stack"
import { isLast } from "lib/shared/utils/isLast"
export const dotSeparator = "•"
export const slashSeparator = "/"
export interface StackSeparatedByProps extends StackProps {
separator: ReactNode
}
export const StackSeparatedBy = ({
children,
separator,
gap = 8,
wrap = "wrap",
...rest
}: StackSeparatedByProps) => {
const items = React.Children.toArray(children)
return (
<Stack wrap={wrap} gap={gap} {...rest}>
{items.map((child, index) => {
if (isLast(items, index)) {
return child
}
return (
<Fragment key={index}>
{child}
{separator}
</Fragment>
)
})}
</Stack>
)
}
export interface HStackSeparatedByProps
extends Omit<StackSeparatedByProps, "direction"> {}
export const HStackSeparatedBy = ({
alignItems = "center",
...props
}: HStackSeparatedByProps) => {
return <StackSeparatedBy direction="row" alignItems={alignItems} {...props} />
}
To add lines between sections, we have the SeparatedByLine
component. While it's also built on top of the Stack
component, it uses CSS to add a border-bottom
and padding-bottom
to all children except the last one. Here, we don't want to iterate over elements because sections are usually more complex components, and they might return null
. In such cases, we won't be able to recognize it because children would still be a component.
import styled, { css } from "styled-components"
import { VStack } from "./Stack"
import { getCSSUnit } from "./utils/getCSSUnit"
import { getColor } from "./theme/getters"
export const SeparatedByLine = styled(VStack)`
> *:not(:last-child) {
border-bottom: 1px solid ${getColor("backgroundGlass2")};
padding-bottom: ${({ gap = 0 }) => getCSSUnit(gap)};
}
`