← Design system

Actions & Buttons

Three slots, each with one job. Links go somewhere. Buttons do a thing. data-action='featured' turns either into the big-bold rectangle people are trained to look for — without erasing whether it's a link or a button underneath.

Scope: this is for content-page actions — links, form submits, CTAs. UI controls (mode toggles, focus switchers, theme/palette setters, completion markers) live under mise en mode, a separate pattern with its own visual language. Don't reach for data-action='featured' on a UI control.

The stance

<a>
Goes somewhere. Inside prose (<p>) it auto-styles as a link — accent color, underline. Outside prose, bare <a> renders as plain text. The "how do links look outside prose" question is unsettled — see note below.
<button>
Does a thing (submit, trigger). Locked to a cross-browser native-feel chrome (subtle bevel, reads tokens from its scope so it adapts to chrome dark / palettes). Opt into data-action='featured' when it's a primary action.
<summary>
Toggles a <details> disclosure. It is an action — clicking it expands or collapses content. Opt into data-action='featured' when the disclosure is a real CTA on the page (e.g. the "Read more" trigger on optional_reading_module).
data-action='featured'
The loud rectangle. Element-agnostic — works on any clickable. Says "this is the action to notice."
.unstyled
Escape hatch. Strips all chrome — for icon buttons, close-X, custom toggles.
.actions
Wrapper for grouping one or more actions. Flex row, small gap. Keeps buttons / links from stretching across grid columns and aligns them horizontally. Reach for it whenever you have actions outside prose — form footers, page CTAs, kebab panels.

Open question: link styling currently only fires inside <p> via the p a rule in theme-setup.css. Outside prose (in .actions, in headers, in widgets), <a> falls back to plain text. The class .link is already heavily used in the codebase as a generic nav-link marker (site menu, footer, etc.) so it can't be the opt-in. A separate piece of work for the /design-system/controls/links page when we get to it.

Defaults

Inside a paragraph, a link looks like a link automatically — accent color, underline. Outside prose (in an .actions container, a header, a widget), bare <a> renders as plain text. That's the open question flagged above.

Plain <button> uses a cross-browser native-feel chrome — subtle bevel, reads tokens from its scope (so it adapts to chrome dark and palettes). If it needs to be louder, that's what data-action='featured' is for.

A link with class='link'

Featured prominence

Both elements get the big-rectangle treatment, but the underlying type-ness still reads. The link keeps the prose font and its underline — serif, link-flavored. The button keeps the browser's sans-serif with no underline — sans, button-flavored. Same shape, same loudness, two different things.

Featured link

Use sparingly. One per section, ideally one per page. The whole point of "featured" is that something else isn't.

Featured destinations

Featured links carry the same external-link tell as inline links: an arrow when the destination is off-site or opens in a new tab. In-site links stay clean.

The exclusion list covers perpetual.education and the local pe:8888 dev host — see a[data-action='featured'] in styles/components.css.

Side by side

Every action element at every emphasis level. A bare <summary> isn't a standalone control — it only makes sense inside a real <details>, so it shows up only at the featured level (the loud disclosure pattern).

Link Featured link
Featured summary

(disclosure body)

Focused

Each action type in its :focus-visible state. Shown statically here via data-fakestate='focused' so the focus treatment is visible without tabbing.

Featured link
Featured summary

(disclosure body)

Summary as an action

A <summary> inside <details> is genuinely an action — clicking it toggles disclosure. With data-action='featured' it picks up the same loud-rectangle treatment as a link or a button. The default disclosure marker is hidden so the rectangle reads as a control, not a list item.

Read more

This is the kind of disclosure that lives at the bottom of a workshop — an "extras for the curious" slot. The optional_reading_module uses exactly this pattern.

Compare this with a button or link — same loud shape, same role (an action you can take), but the underlying element is a real <summary> tied to a real <details>. Click the trigger again to collapse.

The .actions container

Group controls in a <footer class='actions'> (or any element with class='actions'). Provides flex layout, wrapping, and consistent gap.

Admin-flavored grouping uses data-tone='admin':

Unstyled escape hatch

For icon-only buttons, close-X widgets, and custom toggles where the default chrome would fight the design. Adds class='unstyled':

plain text link, no underline

If you reach for .unstyled on more than a few one-offs, it's a signal that there's a real component pattern to extract (an icon button, a chip, etc.) — not just a missing class.

Why data-action and not .cta?

Three reasons.

First, it's a property, not a category. Calling it .cta would imply the loud rectangle is a thing (a CTA-thing) when really it's just an emphasis level on an action. Keeping it as a data attribute on a real <a>, <button>, or <summary> keeps the semantic distinction visible underneath.

Second, it's element-agnostic. The same attribute works on a link, a button, a disclosure summary, an input submit — anything that's an action. A class-per-element approach (.button-cta, .link-cta) fragments the same idea across many names.

Third, data-action is distinct from data-prominence, which the codebase reserves for layout emphasis on <section> and <page-module> (see styles/prominence.css). One attribute per concept — action emphasis vs. layout emphasis — avoids the muddle of the same word meaning two different things in two different scopes.

This is, frankly, a concession to convention. Users have been trained by the rest of the web to look for the loud rectangle. We give them one — but we don't pretend it's the natural shape of an action.

Migration status

Migration complete. The legacy .button class and the button.interface marker have been retired — all template instances now use data-action='featured' for the loud rectangle, and bare <button> falls through to the browser default.

The variant classes .secondary and .small (previously .button.secondary, .button.small) have been preserved on the migrated elements but their CSS was retired with .button. They render no visual difference today — flagged for the variant decision when the production CSS lands in components.css.

The action-message component has its own component-scoped prominence values (filled, etc.) that are not the same system. Left alone for now; flagged for future harmonization.