Typography and surfaces
Semantic typography, surfaces, and server-safe rendering of explicitly imported Lucide and Faye icon artwork.
Quick start
After the application setup, copy this component into your application. It imports only public packages; the complete live demo later on this page also uses documentation layout helpers.
import { Card, CardHeader, CardTitle, CardDescription, CardBody } from '@fayegram-ai/faye-ui/core/surface';
import { Text } from '@fayegram-ai/faye-ui/core/text';
export function StoryCard() {
return <Card>
<CardHeader>
<CardTitle>A little room for wonder</CardTitle>
<CardDescription>Ideas for your next day out.</CardDescription>
</CardHeader>
<CardBody><Text as="p">Choose a place and make it your own.</Text></CardBody>
</Card>;
}Usage contract
Static primitives can execute on the server without a client theme hook. Use explicit profiles for their nonvisual defaults. Card sections preserve structure; an icon requires an accessible name when it conveys meaning.
Customization
Use semantic tokens for visual changes, public component parts for structural customization, and explicit props for instance behavior. The surrounding site lets you compare light and dark, different brands and density.
Source and verification
The examples below import actual public package entries. Full source is downloadable. Source coverage does not establish keyboard, hydration or browser compatibility; those require the application test suite.
Semantic typography
Text and Heading support display, six heading roles, body/body-large, caption, label, overline and code. A visual role controls family, size, weight, line height and tracking. It never implicitly changes the semantic element or heading level. Explicit profiles and type augmentation remain available for server-safe use.
Faye UI icon system
Faye UI uses two independent artwork sources behind one server-safe SVG renderer. @lucide/icons@1.52.0 supplies pinned upstream data without a fork. @fayegram-ai/faye-ui/icons supplies Faye-owned data; its initial FayeMark is the organization mark and preserves the existing favicon letterform. @fayegram-ai/faye-ui/contracts/icon defines and validates portable artwork. @fayegram-ai/faye-ui/core/icon renders either source and imports neither collection.
Installation and rendering
Install @fayegram-ai/faye-ui with its declared React peers. Import the renderer from @fayegram-ai/faye-ui/core/icon and organization artwork from @fayegram-ai/faye-ui/icons; these are module paths inside that one package. Upstream artwork uses the pinned @lucide/icons@1.52.0 data package. The package is currently unpublished; see installation for availability.
import { Search } from '@lucide/icons';
import { FayeMark } from '@fayegram-ai/faye-ui/icons';
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
export function ArtworkExample() {
return <>
<Icon icon={Search} size="sm" />
<Icon icon={FayeMark} size="md" label="Faye" />
</>;
}Faye UI supports typed per-icon entrypoints, for example import { FayeMark } from '@fayegram-ai/faye-ui/icons/faye-mark'. Use Lucide's typed named root exports; the pinned raw package's icons/* entries do not supply individual declarations. Both root imports preserve tree shaking in the installed consumers. Avoid a namespace import, the upstream icons catalogue object, dynamic name imports, or a global registry in application/library code. A local lookup map containing explicitly imported entries is supported and retains those entries only.
The renderer produces one SVG and requires no client provider or browser state. It works in client, SSR and RSC consumers. React 19 ref reaches the SVG through ordinary prop forwarding. Normal public entries own their local CSS; native Node SSR uses the faye-ui-ssr condition and the host delivers CSS separately, as with other Faye UI primitives.
Inputs and property precedence
Use either icon data or SVG children. TypeScript rejects both inputs together; runtime rendering also rejects that combination. Existing children-based calls remain supported:
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
export function CustomActionArtwork() {
return <Icon size="sm" label="Custom action">
<path d="M12 5v14M5 12h14" />
</Icon>;
}Explicit SVG rendering props override definition root defaults, which override renderer defaults. Node-local drawing attributes retain ordinary SVG inheritance; root props do not erase deliberately filled paths, local transforms, or local stroke choices. Explicit viewBox overrides the definition's viewBox. An omitted definition viewBox derives from size, paired width/height, or the 24-unit default. Non-square coordinate spaces keep SVG's normal aspect-ratio behavior.
New outlined data defaults to fill="none", stroke="currentColor", a 2-unit stroke and round caps/joins. Legacy children retain their 1.6-unit stroke. Filled definitions such as FayeMark supply fill="currentColor" and stroke="none"; their viewBoxes and drawing attributes are preserved. Set a root color/CSS color to affect inherited currentColor, or explicit drawing props for a specific rendering choice.
Sizes xs, sm, md, and lg use 12, 16, 18, and 24px intrinsic frames, with corresponding --faye-ui-size-icon-* CSS tokens. Coordinate dimensions describe artwork, not control height. Token styles determine the displayed frame when CSS is present. Explicit CSS sizing can override that frame; width/height attributes remain intrinsic fallback dimensions. Icons do not add control padding or define pointer targets.
Accessibility and direction
Explicit ARIA attributes take precedence. Otherwise label supplies aria-label and a named icon defaults to role="img". An unnamed icon defaults to aria-hidden="true" and focusable="false". Give an icon-only button its action name on the button and leave the nested artwork decorative. Use aria-labelledby for a shared visible name. Callers control intentional role, focusability, or hidden-state overrides.
Mirroring is opt-in: mirrorInRtl overrides optional definition rtl: 'mirror'; definitions without either stay unchanged. Set dir="rtl"/dir="ltr" on an ancestor or the SVG itself. Faye UI's directional scopes already emit those attributes. The inherited CSS direction variable handles nested overrides without a provider. dir="auto" and CSS-only direction changes are not the mirroring contract; use an explicit direction attribute.
The renderer mirrors an inner artwork group around its viewBox center. Outer SVG positioning transforms and authored nested transforms remain intact. Pagination/back/navigation arrows request mirroring. Nondirectional checks, close marks and search icons do not. NativeSelect still restores its system arrow in forced-colors mode; generic artwork inherits system colors through currentColor. Automated coverage does not replace human forced-colors or screen-reader review.
Portable custom artwork
Definitions contain plain data, not React elements, functions, event handlers or browser state. The shared contract supports nested g nodes, path, circle, rect, line, ellipse, polyline, and polygon. It supports positive square size or paired width/height, a four-number viewBox, root drawing defaults, optional aliases/name, and optional RTL behavior.
import type { IconDefinition } from '@fayegram-ai/faye-ui/contracts/icon';
export const ExampleArtwork = {
name: 'example-artwork',
width: 32,
height: 16,
viewBox: [0, 0, 32, 16],
defaults: { fill: 'currentColor', stroke: 'none' },
node: [['g', { transform: 'translate(1 1)' }, [
['rect', { x: 0, y: 0, width: 8, height: 6 }],
]]],
} as const satisfies IconDefinition;validateIconDefinition(definition) validates authored data without React/DOM and returns the same object. The renderer validates data before creating SVG nodes. Both camelCase and ordinary hyphenated SVG drawing attributes are supported. Geometry and drawing attributes are allowlisted; key, class/className, and local transform are also supported on nodes. Root defaults accept drawing attributes only.
Unsupported data includes embedded SVG wrappers, text, XML, scripts, foreignObject, use/href, defs, gradients, filters, masks, clipPath resources, animation nodes, URL paint references, event attributes, and style objects. Limits are 512 nodes, depth 16, and 65,536 characters per string attribute. Numbers must be finite; dimensions must be positive. Do not cast unsupported data to silence TypeScript. The plain-data restrictions apply to definitions; supported legacy children remain a caller-authored React/SVG compatibility path.
Authoring custom artwork
Create a named constant satisfying IconDefinition, then pass it to Icon through its icon property. Validate it with validateIconDefinition before rendering. Keep the artwork's coordinate space explicit, use drawing attributes supported by the validator, and add RTL metadata only when direction changes its meaning. Your application owns custom artwork licensing and attribution.
The renderer imports neither artwork catalog automatically. Choose only the definitions your application needs. Upgrade an external icon dependency deliberately and check its type, geometry and license changes.
The package's LICENSE retains applicable upstream ISC and Feather MIT notices. Faye-owned artwork uses MIT. Raw Lucide artwork remains an external dependency.
Labeled separators
Separator accepts children for a visible section label and textAlign="start", "center" or "end". Decorative lines remain separate from meaningful text. Set decorative={false} when the separator itself needs structural semantics. Both horizontal and vertical separators remain available.
Live examples
Try the controls below, then read or copy the complete example to adapt it for your application.
Typography and semantic structure
Structure before decoration
A readable baseline for product interfaces.
Use semantic roles; the same tokens resolve in both schemes.
color.text is a meaning, not a palette index. Press Tab to explore.
A deliberately long title that may be truncated in a constrained layout while preserving the original text.
Additional screen-reader context.Icon artwork and compatibility
Surfaces and cards
Research workspace
Header, body and footer share spacing, not business rules.
Read the complete example
import { Plus, Search, ArrowLeft } from '@lucide/icons';
import { FayeMark } from '@fayegram-ai/faye-ui/icons';
import { ThemeScope } from '@fayegram-ai/faye-ui/provider/provider';
import { ScopedBaseline } from '@fayegram-ai/faye-ui/core/scoped-baseline';
import { Box } from '@fayegram-ai/faye-ui/core/box';
import { Text, Heading } from '@fayegram-ai/faye-ui/core/text';
import { Surface, Card, CardHeader, CardBody, CardFooter } from '@fayegram-ai/faye-ui/core/surface';
import { Separator } from '@fayegram-ai/faye-ui/core/separator';
import { Code, Kbd } from '@fayegram-ai/faye-ui/core/code';
import { Icon } from '@fayegram-ai/faye-ui/core/icon';
import { VisuallyHidden, SkipLink } from '@fayegram-ai/faye-ui/core/visually-hidden';
import { Inline, Stack } from '@fayegram-ai/faye-ui/layout/layout';
import { Section } from '../example';
export default function Foundations() {
return <><Section title="Typography and semantic structure"><ScopedBaseline><Box as="section" p="2"><Stack gap="3"><Heading level={4}>Structure before decoration</Heading><Text as="p" size="lg">A readable baseline for product interfaces.</Text><Text as="p" tone="muted">Use semantic roles; the same tokens resolve in both schemes.</Text><Text as="p" size="sm"><Code>color.text</Code> is a meaning, not a palette index. Press <Kbd>Tab</Kbd> to explore.</Text><Inline><Icon icon={Plus} label="Add item"/><Text>Import artwork explicitly; icons inherit color through the shared renderer.</Text></Inline><Separator /><Text as="p" truncate title="A long title remains available through native title">A deliberately long title that may be truncated in a constrained layout while preserving the original text.</Text><VisuallyHidden>Additional screen-reader context.</VisuallyHidden></Stack></Box></ScopedBaseline></Section><Section title="Icon artwork and compatibility"><Stack gap="3"><Inline wrap>{(["xs", "sm", "md", "lg"] as const).map(size => <Inline key={size}><Icon icon={Search} size={size} label={`Search ${size}`}/><Icon icon={FayeMark} size={size} label={`Faye ${size}`}/><Text size="sm">{size}</Text></Inline>)}</Inline><ThemeScope direction="rtl"><Inline><Icon icon={ArrowLeft} size="sm" label="Directional action" mirrorInRtl style={{transform:"translateY(1px)"}}/><Text size="sm">Directional artwork mirrors inside its positioned frame.</Text></Inline></ThemeScope><Inline><Icon size="sm" label="Legacy custom action"><path d="M12 5v14M5 12h14"/></Icon><Text size="sm">SVG children remain supported with their original stroke default.</Text></Inline></Stack></Section><Section title="Surfaces and cards"><Inline wrap>{(['plain', 'subtle', 'outlined', 'raised'] as const).map(variant => <Surface key={variant} variant={variant} padding="md"><Text>{variant}</Text></Surface>)}</Inline><Card><CardHeader><Heading level={4} size="sm">Research workspace</Heading></CardHeader><CardBody><Text as="p" tone="muted">Header, body and footer share spacing, not business rules.</Text></CardBody><CardFooter><Text size="sm">Updated locally</Text></CardFooter></Card><SkipLink href="#live-example">Skip this preview</SkipLink></Section></>;
}