Skip to content

Numbered feature list

An ordered list for features that are genuinely sequential or ranked.

Marketingstarterfeaturesorderedsequencemethod

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { Container } from '@/components/ui/layout'

/**
 * Numbered feature list
 *
 * For features that are genuinely sequential or ranked. Rendered as an ordered
 * list, because the numbers are content — using a `<div>` with a styled digit
 * throws away the only structural cue that this is a sequence.
 */
const entries = [
  {
    title: 'Start from tokens, not components',
    body: 'Colour, type, spacing, radius, shadow, motion and density come first. Components are downstream of them, which is why a rebrand touches one file.',
  },
  {
    title: 'Build the twenty components that matter',
    body: 'Twenty primitives cover roughly eighty per cent of screens. Ship those with real states before adding the twenty-first.',
  },
  {
    title: 'Compose sections, do not design them again',
    body: 'A section is an arrangement of primitives. When a new page needs a new section, that is a composition problem, not a design problem.',
  },
  {
    title: 'Prove it on a real route',
    body: 'Migrate one production page before writing the migration guide. The guide gets shorter and truer.',
  },
]

export default function NumberedListFeatures() {
  return (
    <section className="border-b border-line bg-canvas py-section">
      <Container>
        <div className="grid gap-10 lg:grid-cols-[0.8fr_1.2fr] lg:gap-16">
          <div>
            <p className="label-caps text-accent">The order matters</p>
            <h2 className="display-type mt-3 text-2xl font-semibold text-ink-strong sm:text-3xl">
              Four steps, in this order, every time.
            </h2>
            <p className="mt-4 text-md text-ink-muted">
              Most design-system efforts fail by starting at step two.
            </p>
          </div>

          <ol className="flex flex-col">
            {entries.map((entry, index) => (
              <li
                key={entry.title}
                className="flex gap-5 border-b border-line-subtle py-6 first:pt-0 last:border-0 last:pb-0"
              >
                <span className="display-type shrink-0 text-2xl font-semibold text-ink-subtle tabular-nums">
                  {String(index + 1).padStart(2, '0')}
                </span>
                <div className="min-w-0">
                  <h3 className="text-md font-semibold text-ink-strong">{entry.title}</h3>
                  <p className="mt-1.5 text-sm leading-relaxed text-ink-muted">{entry.body}</p>
                </div>
              </li>
            ))}
          </ol>
        </div>
      </Container>
    </section>
  )
}

components/blocks/sections/features/numbered-list.tsx

import { Container } from '@/components/ui/layout'

/**
 * Numbered feature list
 *
 * For features that are genuinely sequential or ranked. Rendered as an ordered
 * list, because the numbers are content — using a `<div>` with a styled digit
 * throws away the only structural cue that this is a sequence.
 */
const entries = [
  {
    title: 'Start from tokens, not components',
    body: 'Colour, type, spacing, radius, shadow, motion and density come first. Components are downstream of them, which is why a rebrand touches one file.',
  },
  {
    title: 'Build the twenty components that matter',
    body: 'Twenty primitives cover roughly eighty per cent of screens. Ship those with real states before adding the twenty-first.',
  },
  {
    title: 'Compose sections, do not design them again',
    body: 'A section is an arrangement of primitives. When a new page needs a new section, that is a composition problem, not a design problem.',
  },
  {
    title: 'Prove it on a real route',
    body: 'Migrate one production page before writing the migration guide. The guide gets shorter and truer.',
  },
]

export default function NumberedListFeatures() {
  return (
    <section className="border-b border-line bg-canvas py-section">
      <Container>
        <div className="grid gap-10 lg:grid-cols-[0.8fr_1.2fr] lg:gap-16">
          <div>
            <p className="label-caps text-accent">The order matters</p>
            <h2 className="display-type mt-3 text-2xl font-semibold text-ink-strong sm:text-3xl">
              Four steps, in this order, every time.
            </h2>
            <p className="mt-4 text-md text-ink-muted">
              Most design-system efforts fail by starting at step two.
            </p>
          </div>

          <ol className="flex flex-col">
            {entries.map((entry, index) => (
              <li
                key={entry.title}
                className="flex gap-5 border-b border-line-subtle py-6 first:pt-0 last:border-0 last:pb-0"
              >
                <span className="display-type shrink-0 text-2xl font-semibold text-ink-subtle tabular-nums">
                  {String(index + 1).padStart(2, '0')}
                </span>
                <div className="min-w-0">
                  <h3 className="text-md font-semibold text-ink-strong">{entry.title}</h3>
                  <p className="mt-1.5 text-sm leading-relaxed text-ink-muted">{entry.body}</p>
                </div>
              </li>
            ))}
          </ol>
        </div>
      </Container>
    </section>
  )
}

components/ui/layout.tsx

import type { ElementType, HTMLAttributes, ReactNode } from 'react'
import { cn } from '@/lib/cn'

/**
 * Layout primitives
 *
 * Five zero-JavaScript building blocks that account for the majority of
 * structure in the library. They exist so that spacing decisions are made once,
 * against density tokens, instead of being re-typed as ad-hoc utilities in
 * every section.
 */

export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {
  /** `prose` narrows to a comfortable reading measure. */
  size?: 'prose' | 'narrow' | 'default' | 'wide' | 'full'
  as?: ElementType
  /** Removes the responsive horizontal gutter. */
  bleed?: boolean
}

const containerSizes = {
  prose: 'max-w-[var(--layout-prose-max)]',
  narrow: 'max-w-3xl',
  default: 'max-w-[var(--layout-content-max)]',
  wide: 'max-w-[110rem]',
  full: 'max-w-none',
} as const

export function Container({
  size = 'default',
  as: Tag = 'div',
  bleed = false,
  className,
  children,
  ...props
}: ContainerProps) {
  return (
    <Tag
      className={cn(
        'mx-auto w-full',
        containerSizes[size],
        !bleed && 'px-4 sm:px-6 lg:px-8',
        className,
      )}
      {...props}
    >
      {children}
    </Tag>
  )
}

export interface StackProps extends HTMLAttributes<HTMLDivElement> {
  direction?: 'row' | 'column'
  gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
  align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
  justify?: 'start' | 'center' | 'end' | 'between' | 'around'
  wrap?: boolean
  as?: ElementType
}

const gapMap = {
  none: 'gap-0',
  xs: 'gap-1',
  sm: 'gap-2',
  md: 'gap-gap',
  lg: 'gap-stack',
  xl: 'gap-8',
} as const

const alignMap = {
  start: 'items-start',
  center: 'items-center',
  end: 'items-end',
  stretch: 'items-stretch',
  baseline: 'items-baseline',
} as const

const justifyMap = {
  start: 'justify-start',
  center: 'justify-center',
  end: 'justify-end',
  between: 'justify-between',
  around: 'justify-around',
} as const

export function Stack({
  direction = 'column',
  gap = 'md',
  align,
  justify,
  wrap = false,
  as: Tag = 'div',
  className,
  children,
  ...props
}: StackProps) {
  return (
    <Tag
      className={cn(
        'flex',
        direction === 'column' ? 'flex-col' : 'flex-row',
        gapMap[gap],
        align && alignMap[align],
        justify && justifyMap[justify],
        wrap && 'flex-wrap',
        className,
      )}
      {...props}
    >
      {children}
    </Tag>
  )
}

export interface GridProps extends HTMLAttributes<HTMLDivElement> {
  /** Column count at the largest breakpoint; smaller breakpoints step down. */
  cols?: 1 | 2 | 3 | 4 | 5 | 6
  gap?: StackProps['gap']
  as?: ElementType
  /** Auto-fit tracks with a minimum width instead of a fixed column count. */
  minItemWidth?: string
}

const colMap = {
  1: 'grid-cols-1',
  2: 'grid-cols-1 sm:grid-cols-2',
  3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
  4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
  5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
  6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
} as const

export function Grid({
  cols = 3,
  gap = 'lg',
  as: Tag = 'div',
  minItemWidth,
  className,
  style,
  children,
  ...props
}: GridProps) {
  return (
    <Tag
      className={cn('grid', minItemWidth ? undefined : colMap[cols], gapMap[gap], className)}
      style={
        minItemWidth
          ? {
              ...style,
              gridTemplateColumns: `repeat(auto-fit, minmax(min(${minItemWidth}, 100%), 1fr))`,
            }
          : style
      }
      {...props}
    >
      {children}
    </Tag>
  )
}

export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
  orientation?: 'horizontal' | 'vertical'
  /** Renders a centred text label interrupting the rule. */
  label?: ReactNode
  weight?: 'subtle' | 'default' | 'strong'
}

const dividerWeight = {
  subtle: 'border-line-subtle',
  default: 'border-line',
  strong: 'border-line-strong',
} as const

export function Divider({
  orientation = 'horizontal',
  label,
  weight = 'default',
  className,
  ...props
}: DividerProps) {
  if (orientation === 'vertical') {
    return (
      <div
        role="separator"
        aria-orientation="vertical"
        className={cn('h-full w-px self-stretch border-l', dividerWeight[weight], className)}
        {...props}
      />
    )
  }

  if (label) {
    return (
      <div className={cn('flex items-center gap-3', className)} {...props}>
        <span className={cn('h-px flex-1 border-t', dividerWeight[weight])} role="separator" />
        <span className="label-caps text-ink-subtle">{label}</span>
        <span className={cn('h-px flex-1 border-t', dividerWeight[weight])} aria-hidden="true" />
      </div>
    )
  }

  return (
    <div
      role="separator"
      className={cn('w-full border-t', dividerWeight[weight], className)}
      {...props}
    />
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Rendered as an ol because the numbers are content. Using a div with a styled digit throws away the only structural cue that this is a sequence.

  • Only number things that are actually ordered.
  • Zero-pad the ordinals so the column edge stays straight past nine.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Two-column layout
  • Large ordinal numerals
  • Hairline separated rows

Accessibility

Ordered list
Position and count are announced by assistive tech.
Numerals
Ordinals use tabular figures so they align vertically.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.