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
titlerenders 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.titleAsstates another level,h1for the band whose heading is the page's title,h3for one nested under a section heading, andheadingSizechanges the treatment without touching the level. A band with notitlerenders no heading block rather than an empty one.- The
<section>the band draws is named by that heading througharia-labelledby, so a reader navigating by region hears it. Without atitlethere is nothing to point at and the section stays unnamed, so passaria-labelwhen a title-less list should still be reachable as a region. - The features are a list, a
ulwith oneliper card, so a reader hears how many there are and which one they are on before reading it. - A feature with an
hrefis 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 emptyalt, 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.