Skip to contentAsheeUIAsheeUI

FeatureGrid

A wall of features, an icon, a name and one sentence each, in a column count the page decides.

A feature grid is the band a landing page reaches for once the hero has made its promise: the short list of what the product actually does, each capability with an icon, a name and a sentence, laid out so a reader can scan it rather than read it. It is where the page stops claiming and starts showing.

Reach for it when three to six capabilities deserve equal weight. One capability is a paragraph, a dozen is a table, and anything that needs a price beside it belongs in the pricing band instead.

Usage

import { FeatureGrid } from "@asheeui/web";

<FeatureGrid
  title="Everything a campaign needs"
  description="Templates, timing and the numbers that follow."
  items={[
    { title: "Templates", description: "Start from a message that already works." },
    { title: "Scheduling", description: "Send when the reader is awake." },
    { title: "Reporting", description: "See what landed, per segment." },
  ]}
/>

items is the only required prop, and each feature is a value rather than markup: an optional icon, a title, an optional description, and an optional href. An item may also state an id, which becomes its key when a builder can reorder the list; without one, the position in the array is used.

Around the features, eyebrow, title and description are the heading block every marketing band shares, and children renders below the grid, which is where a Button leading to the full feature list usually goes. The band is a Section, so spacing is its vertical room and background is the surface behind the cards.

The column count

The three counts are the layout family's column scale: 1, 2, 3, 4, 6 or 12. Read them from narrow to wide, because the breakpoints are min-widths, columns is the count at the smallest viewport, columnsMd takes over from md, columnsLg from lg, and a wide screen is answered by columnsLg whatever columnsMd states.

Each of the three resolves on its own, and all three have a registered default (one, two, three). That is the difference from a bare Grid: stating columns={2} and nothing else gives two columns at the narrowest width, then two at md and three at lg, not two everywhere. A band that should keep one count at every width states all three.

The count is also the thing to tune once the copy exists. Four features fill their rows at 2 and at 4 and leave a gap at 3; five leave a gap at every count, because nothing in the scale divides five. Change the count, or the number of features, rather than letting the last row be short by accident.

Features that go somewhere

An href makes the whole card the destination. The card renders the anchor itself, a native <a>, or the component link names, rather than a surface holding a handler, so the pointer, the keyboard and the announcement agree about what the card is:

  • Hover tints the border and adds a shadow across the whole card, and the pointer is a hand over all of it; a press scales the surface by a hair.
  • The card is the tab stop, so focus draws its ring around the whole card rather than around the words inside it.
  • A reader hears one link named by everything the card contains, title and description together, so the title is the label, and a description written as a second, separate call to action would be read as part of it.

A feature with no href is the same card without any of that: a div with no role, no tab stop and no press state. It is a description, not a control, so give it a description's copy, and put the destination on the feature that has one.

link is not an action. This band has no primaryAction or secondaryAction; those belong to the bands that open and close a page. link changes only what the anchor each feature renders is made of, component for a router's own link, props for the props it needs, and it applies only to features that state an href, because a card with no destination renders no anchor to replace.

A feature's destination is the item's href, and the band passes it to the component as href unless mapProps says otherwise. That is the whole adapter: a router that names the same thing to is stated once, for every card, rather than wrapped component by component:

import { Link } from "@tanstack/react-router";

// The band hands each feature to `mapProps`; the router names the destination `to`.
<FeatureGrid
  link={{ component: Link, mapProps: (item) => ({ to: item.href }) }}
  items={[
    { title: "Templates", description: "Start from a message that works.", href: "/templates" },
  ]}
/>

mapProps receives the feature and its position, and what it returns is merged over link.props and replaces the default href. A card whose feature states no href still renders no anchor, so mapProps is never asked about one.

Props

The defaults are a cascade rather than a list. All nine options are registered under components.featuregrid, columns: 1, columnsMd: 2, columnsLg: 3, gap: "lg", spacing: "lg", background: "none", align: "center", containerSize: "lg" and contained: true, so each resolves from the instance, then that configuration section, then the registered value. State the count once there and every band in the product follows; state it on one band and only that band changes. align moves the heading block alone, and contained wraps the heading and the grid in a Container of containerSize while the band itself still spans the page.

A page with no configuration gets a centred heading over one column of cards with a lg gap, in a lg-spaced band on no background. What the band does not decide is the surface: every feature is a bordered Card at md density, so its corner radius comes from the application's defaultRadius and the card's own components.card settings, and a product that rounds everything to full gets rounded feature cards without this band knowing. items, link, children, as and the heading's content, eyebrow, title, description and the two options that place it in the outline, titleAs and headingSize, are instance-only, because they state this page's features and this page's outline rather than the design's.

Accessibility

  • title renders a level-two heading by default and each feature's title a level three, so the grid sits under the page's own <h1> and every feature is a heading of its own. titleAs states another level, h1 for the band whose heading is the page's title, h3 for one nested under a section heading, and headingSize changes the treatment without touching the level. A band with no title renders no heading block rather than an empty one.
  • The <section> the band draws is named by that heading through aria-labelledby, so a reader navigating by region hears it. Without a title there is nothing to point at and the section stays unnamed, so pass aria-label when a title-less list should still be reachable as a region.
  • The features are a list, a ul with one li per card, so a reader hears how many there are and which one they are on before reading it.
  • A feature with an href is an anchor: announced as a link, named by its own title and description, reached with Tab, and openable in a new tab or copyable as a URL. A feature without one has no role and no tab stop, so it is read as text.
  • The icon is rendered as given, before the title. Give an inline SVG aria-hidden, or a picture an empty alt, so a feature's name stays its words; anything the glyph itself carries belongs in the title.

The heading block, the data-not-markup decision and the one-h1 rule are shared by every band, see Marketing sections.