All components roundedsmmd
Grid List
Interactive row lists with a sticky header, hover states, and rounded rows.
Recent deployments
- web-app2m ago
- docs-site1h ago
- api-server3h ago
1234567891011121314151617181920212223242526272829import { GridList } from "#components/ui/base-ui/grid-list.tsx";
const deployments = [ ["web-app", "2m ago"], ["docs-site", "1h ago"],];
export function Example() { return ( // variant/size resolve once on Root; parts inherit them via context and // expose data-* attributes. Items is a reset <ul>; rows are <li> and are // interactive (hover/active) by default — pass interactive={false} to // opt out. <GridList.Root variant="rounded" size="md"> <GridList.Header>Recent deployments</GridList.Header> <GridList.Items> {deployments.map(([name, meta]) => ( <GridList.Item key={name} onClick={() => open(name)}> <GridList.Cell>{name}</GridList.Cell> <GridList.Cell>{meta}</GridList.Cell> </GridList.Item> ))} </GridList.Items> </GridList.Root> );}
// composition works per part — e.g. render a row as a link:// <GridList.Item render={<a href="/deploys/web-app" />}>…</GridList.Item>Variants
Recent deployments
- web-app2m ago
- docs-site1h ago
- api-server3h ago
Sizes
Recent deployments
- web-app2m ago
- docs-site1h ago
- api-server3h ago
Recent deployments
- web-app2m ago
- docs-site1h ago
- api-server3h ago
Install
1npx stylexblocks add grid-listSource
components/ui/base-ui/grid-list.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215import * as stylex from "@stylexjs/stylex";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe } from "#lib/recipe-props.ts";import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticShadows } from '@stylexblocks/ui/semantic-tokens/shadows.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const gridListRoot = stylex.create({ base: { position: 'relative', },})
const gridListRootSizes = stylex.create({ sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, paddingBlock: spacing._0_5, }, md: { fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingBlock: spacing._1, },})
const gridListItem = stylex.create({ base: { display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flex: 1, userSelect: 'none', borderRadius: 'inherit', outline: 'none', boxShadow: { default: null, ':focus-visible': `0 0 0 2px ${semanticColors.accentFocusRing}`, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': cursor.disabled, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, backgroundColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': { default: null, ':hover': { default: 'transparent', '@media (prefers-color-scheme: dark)': 'transparent', }, ':active': { default: 'transparent', '@media (prefers-color-scheme: dark)': 'transparent', }, }, }, },})
const gridListItemSizes = stylex.create({ sm: { paddingBlock: spacing._1, paddingInline: spacing._2, gap: spacing._1, }, md: { paddingBlock: spacing._2, paddingInline: spacing._3, gap: spacing._2, },})
const gridListItemVariants = stylex.create({ rounded: { borderRadius: radii.md, marginBottom: spacing._0_5, },})
const gridListItemInteractives = stylex.create({ true: { cursor: { default: cursor.button, ':is(:disabled, [disabled], [data-disabled])': cursor.disabled, }, transitionProperty: 'bg', transitionDuration: durations.fast, backgroundColor: { default: null, ':is(:disabled, [disabled], [data-disabled])': { default: null, ':hover': { default: 'transparent', '@media (prefers-color-scheme: dark)': 'transparent', }, ':active': { default: 'transparent', '@media (prefers-color-scheme: dark)': 'transparent', }, }, ':hover': semanticColors.bgSubtle, ':active': semanticColors.bgSubtle, }, },})
const gridListHeader = stylex.create({ base: { display: 'flex', flexDirection: 'row', position: 'sticky', fontWeight: fontWeights.medium, color: semanticColors.fgSubtle, },})
const gridListHeaderSizes = stylex.create({ sm: { paddingBlock: spacing._0_5, paddingInline: spacing._2, }, md: { paddingBlock: spacing._1, paddingInline: spacing._3, },})
const gridListCell = stylex.create({ base: { flexShrink: 0, },})
export type GridListInteractive = keyof typeof gridListItemInteractives
export type GridListVariant = keyof typeof gridListItemVariants
export type GridListSize = keyof typeof gridListRootSizes
// Rows render as real <li> inside a GridList.Items <ul>; the reset lives// here because the recipe styles only the rows themselves.const gridListLocal = stylex.create({ items: { listStyle: "none", marginBlock: 0, paddingInlineStart: 0 },});
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axis.// (The generated recipe's own default is the unstyled "simple" variant, which// has no map entry — "rounded" is the only styled variant, so it is the// component default.)const gridListSlotRecipe = defineSlotRecipe({ slots: { root: { styles: gridListRoot, sizes: gridListRootSizes }, header: { styles: gridListHeader, sizes: gridListHeaderSizes }, item: { styles: gridListItem, sizes: gridListItemSizes, variants: gridListItemVariants }, cell: { styles: gridListCell }, }, defaultVariants: { variant: "rounded", size: "md" },});
/** * GridList — recipe and component colocated in one file. `<GridList.Root * variant size>` resolves the recipe once and every part inherits it through * context; each part composes via `render`. `GridList.Item` takes a boolean * `interactive` prop (default true) that layers the hover/active row styles * from the recipe's interactive map. */const { withProvider, withContext } = createSlotRecipeContext(gridListSlotRecipe);
const GridListItemBase = withContext( "li", (v) => [ gridListItem.base, gridListItemSizes[v.size], v.variant === "rounded" && gridListItemVariants.rounded, ], "GridListItem",);
function GridListItem({ interactive = true, style, ...props}: React.ComponentProps<typeof GridListItemBase> & { /** Cursor, hover, and active row treatment from the recipe. */ interactive?: boolean;}) { return ( <GridListItemBase {...props} style={[interactive && gridListItemInteractives.true, style] as never} /> );}
export const GridList = { Root: withProvider("div", "root", "GridListRoot"), Header: withContext("div", "header", "GridListHeader"), Items: withContext("ul", () => [gridListLocal.items], "GridListItems"), Item: GridListItem, Cell: withContext("span", "cell", "GridListCell"),};