Most landing pages want one visual headline style that has to render as <h1> at the top of the page, <h2> inside a section, and sometimes <span> inside a card. Plain CSS-in-JS usually solves this with one component per tag, all importing the same styles — which gets old fast.
Salty CSS's styled() takes an element option for the default tag, and the rendered component accepts an as prop for per-instance overrides. The styles compile once.
The component
// /components/heading.css.ts
import { styled } from "@salty-css/react/styled";
// Default tag is <h2>; `as` swaps it at the call site.
export const Heading = styled("h2", {
base: {
fontFamily: "system-ui, sans-serif",
fontWeight: 700,
lineHeight: 1.1,
letterSpacing: "-0.02em",
margin: 0,
},
variants: {
size: {
sm: { fontSize: "1.25rem" },
md: { fontSize: "1.75rem" },
lg: { fontSize: "2.5rem" },
xl: { fontSize: "3.5rem" },
},
tone: {
default: { color: "{theme.color}" },
muted: { color: "{theme.altColor}" },
brand: { color: "{theme.highlight}" },
},
},
defaultVariants: { size: "md", tone: "default" },
});Using it
The as prop overrides the rendered tag without changing the style props. Variant props (size, tone) stay typed.
import { Heading } from "./heading.css";
const ArticleHeader = () => (
<header>
{/* Renders <h1> for the document outline… */}
<Heading as="h1" size="xl">Polymorphic Heading</Heading>
{/* …and the same visual style as the default <h2> further down. */}
<Heading size="md" tone="muted">A shared headline style, any tag</Heading>
</header>
);
export default ArticleHeader;What it produces
styled("h2", { … }) emits one class per base and per variant. Switching as does not create a new class — Salty just picks a different element type to render into (the framework chooses the right tag at render time). The runtime overhead is the same conditional Salty already does for every styled component (a tiny class-picking helper). No bundle is shipped for the styling.
Gotchas
- Headings are an SEO contract. Pick
asbased on the document outline, not the visual size. The whole point of the recipe is that the two are now independent — use it. - defaultVariants apply before as. Passing
as="span"keeps thesize/tonedefaults; it doesn't reset them. - Wrapping a third-party component? Pass it as the first argument to
styled(...)and addpassPropsfor the props the wrapped component needs. Theasprop still works on the result. See the Overrides reference.