RemixRemix

Typography

Fortal's Text, Heading, Code, Kbd, and Link widgets on one shared nine-step scale

Fortal ships five typography families — FortalText, FortalHeading, FortalCode, FortalKbd, and FortalLink. They share one nine-step size scale (FortalTextSize) and one weight enum (FortalTextWeight) instead of declaring five parallel copies.

These families live only in the application-owned Fortal preset. There is no RemixText or RemixHeading: base Remix stays theme-free, and a TextStyler plus your own tokens is the equivalent there. The examples assume the preset was initialized with --prefix Fortal as shown in the Fortal overview.

For visual reference, open the light matrix or dark matrix. These Orla captures show the current size and variant scales; they are not interactive previews.

Shared type scale

Step123456789
Size (px at 100%)121416182024283660

All values follow theme scaling. Accordion and disclosure labels use the same text2 / text3 tokens as the rest of the system.

Choosing a widget

WidgetUse it forSemantics
FortalTextBody copy, labels, valuesNone; one plain Text node
FortalHeadingPage, section, and card titlesheader with an explicit headingLevel
FortalCodeIdentifiers, snippets, tokens shown inlineNone; Flutter has no code role
FortalKbdOne keyboard key or shortcutkeyboardKey, inert
FortalLinkText that navigateslink only while enabled and given an onPressed

FortalText, FortalCode, FortalKbd, and FortalLink keep nullable public size fields, but an omitted size resolves the active scope's text3 token rather than the ambient DefaultTextStyle. FortalText also defaults to the regular weight token and neutral gray-12; FortalHeading defaults to size 6, bold, and neutral gray-12.

Code and Kbd have compact, token-anchored adjustments: an unsized soft Code resolves 0.95 × 0.95 × text3, and an unsized Kbd resolves 0.75 × text3. Pass an explicit size: to select another token size.

The outermost FortalTheme still establishes the root text run as a courtesy DefaultTextStyle for bare Flutter Text. Under WidgetsApp, put the scope in builder: so that fallback reaches routes and raw overlays; see scope placement. A nearer DefaultTextStyle can replace the run for bare text, but never changes Fortal typography metrics.

A nested FortalTheme re-scopes tokens without restating the courtesy bare-Text run. Fortal typography re-resolves against the nested gray and scaling tokens, while ordinary Flutter text keeps its nearest inherited style.

Basic implementation

Basic implementation

import 'package:flutter/material.dart';
import 'ui/ui.dart';

class TypographyExample extends StatelessWidget {
  const TypographyExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      spacing: 12,
      children: [
        FortalHeading('Release notes', size: FortalTextSize.size6),
        FortalText(
          'Every recipe resolves through the active Fortal scope.',
          size: FortalTextSize.size3,
        ),
        Row(
          spacing: 10,
          children: [
            FortalCode.soft('FortalTheme', size: FortalTextSize.size2),
            FortalKbd.soft(
              '⌘K',
              size: FortalTextSize.size2,
              semanticLabel: 'Command K',
            ),
          ],
        ),
      ],
    );
  }
}

Size, weight, and flow

Every family accepts the same scale and flow controls.

ControlTypeApplies to
sizeFortalTextSize? — size1–size9all five
weightFortalTextWeight? — light, regular, medium, boldText, Heading, Code, Link
alignTextAlign?Text, Heading
softWrapbool (default true)Text, Heading, Code, Link
truncatebool (default false)Text, Heading, Code, Link
accentbool (default false)Text, Heading, Code
highContrastbool (default false)Text, Heading, Code, Link

truncate deliberately wins over softWrap: it forces exactly one ellipsized line, which is what a table cell or a row with a fixed-width neighbour needs.

FortalText and FortalHeading use neutral gray-12 unless accent: true takes the surrounding FortalTheme accent at accent-a11; adding highContrast: true promotes it to accent-12. highContrast alone leaves the neutral color unchanged.

FortalKbd is the exception: it pins its own regular weight, line box, and gray-12 foreground so a key cap never follows the surrounding copy.

Variants

Code and Kbd variants

import 'package:flutter/material.dart';
import 'ui/ui.dart';

class TypographyVariantsExample extends StatelessWidget {
  const TypographyVariantsExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const Wrap(
      spacing: 12,
      runSpacing: 12,
      crossAxisAlignment: WrapCrossAlignment.center,
      children: [
        FortalCode.solid('solid'),
        FortalCode.soft('soft'),
        FortalCode.outline('outline'),
        FortalCode.ghost('ghost'),
        FortalKbd.soft('⌘K', semanticLabel: 'Command K'),
        FortalKbd.soft('Esc', semanticLabel: 'Escape'),
      ],
    );
  }
}

FortalCode.ghost is transparent and inherits the ambient foreground unless you pass accent: true; the other three variants always paint their own accent roles. A foreground composed onto the recipe overrides that ambient fallback.

Headings and semantic level

headingLevel drives the accessibility level only. Changing it never changes the visual size. A page title can be level 1 at size 6 while an empty-state title stays level 2 at size 3.

Heading level and size

import 'package:flutter/material.dart';
import 'ui/ui.dart';

class HeadingLevelExample extends StatelessWidget {
  const HeadingLevelExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      spacing: 8,
      children: [
        FortalHeading('Overview', size: FortalTextSize.size6),
        FortalHeading(
          'Recent activity',
          headingLevel: 2,
          size: FortalTextSize.size4,
          weight: FortalTextWeight.medium,
        ),
      ],
    );
  }
}

FortalHeading publishes exactly one Semantics node with header: true and the requested level, and excludes the child so the label is not announced twice. Pass semanticLabel when the announced text should differ from the rendered text, or excludeSemantics: true to publish nothing at all.

A FortalLink without onPressed is disabled, the same as one with enabled: false — a null callback disables a Flutter control. It keeps the accent colour but gives up its focus stop, link role, and activation. Give it onPressed and it becomes a real link: focusable, activatable with pointer and Enter, and underlined according to underline.

A disabled link never underlines regardless of underline. For accent-coloured text that was never meant to be followed, use FortalText(accent: true).

Space does not activate a link. That is the Button role's key; a link takes Enter, matching an anchor on the web.

underlineBehaviour when actionable
auto (default)Underlines on hover, or always at high contrast
alwaysAlways underlined
hoverUnderlines on hover only
noneNever underlined

A focus-visible outline replaces the underline rather than stacking both, and the outline only appears for keyboard focus — a touch or pointer focus keeps the idle treatment.

Links

import 'package:flutter/material.dart';
import 'ui/ui.dart';

class LinkExample extends StatelessWidget {
  const LinkExample({super.key, required this.onOpenDocs});

  final VoidCallback onOpenDocs;

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 16,
      children: [
        FortalLink(
          'Read the docs',
          underline: FortalLinkUnderline.always,
          linkUrl: Uri.parse('https://docs.page/btwld/remix/fortal'),
          semanticHint: 'Opens the Fortal documentation',
          onPressed: onOpenDocs,
        ),
        const FortalLink('Not available yet'),
      ],
    );
  }
}

linkUrl is assistive metadata only and is never launched. Navigation stays your responsibility inside onPressed, which keeps the owned recipe free of a URL launcher dependency. Passing linkUrl without onPressed asserts, because an announced destination with no activation is a broken promise to assistive technology.

enabled: false keeps the callback and still refuses focus and activation, so a link that is only temporarily unavailable can hold on to its destination.

Customizing the recipes

Each widget calls a fortal*Style recipe you can use directly. FortalText and FortalHeading return a TextStyler; FortalCode and FortalKbd return a BadgeStyler and FortalLink returns a LinkStyler. All three take a BuildContext, because their geometry is em-relative to the resolved font size.

Recipe customization

import 'package:flutter/material.dart';
import 'package:remix/remix.dart';
import 'ui/ui.dart';

/// Body text on the Fortal scale with one project-specific neutral tone.
TextStyler mutedBody() => fortalTextStyle(
  size: FortalTextSize.size2,
  weight: FortalTextWeight.medium,
).color(FortalTokens.gray11());

/// The Fortal Code recipe with a wider inline gutter.
BadgeStyler roomyCode(BuildContext context) =>
    fortalCodeStyle(context, size: FortalTextSize.size2)
        .padding(.horizontal(10));

Starting from the recipe keeps the scale, weights, and flow behaviour Fortal's and layers only what your project actually adds. Reaching for a bare TextStyler instead re-declares the whole type ramp locally.

Composition notes

  • Flutter paints the token line box without leading trim. softWrap and truncate control flow; use layout constraints to select responsive values.
  • FortalCode and FortalKbd are standalone boxes, not inline text spans. Compose them in a Wrap or row alongside text.
  • Link underline thickness follows the font's underline metrics. Keyboard focus uses the shared outside ring and suppresses the underline.
  • Kbd has only the flat soft variant and is inert, with keyboard-key semantics.
  • Scope a named accent or generated brand palette around a subtree to change color without restating its typography.
View page source on GitHub