---
title: Fortal
description: An independent, Fortaleza-inspired design system for Remix
---
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.page/btwld/remix/llms.txt
> Use this file to discover all available pages before exploring further.

**Fortal** is an application-owned design system built on Remix and Naked UI.
Seven city-inspired palettes, neutral defaults, flat controls, and shared token
scales give apps a complete starting point without locking away the source.

![A selection of Fortal components and variants](/assets/fortal-catalog.png)

This overview covers installation and theming. Use the [Catalog](/fortal/catalog)
for widget sizes, variants, and defaults, or [Typography](/fortal/typography)
for text styles. Live examples and their source live on the individual
[component pages](/components/button).

## Why a separate preset

Fortal lives in its own application-owned preset so that `remix` stays a
genuinely theme-free component library. If you are building your own design
system on Remix, you pay nothing for a theme you never use. If you choose
Fortal, the CLI copies its token tables, owned color palettes, and recipes into your
application, where they become your source.

The repository keeps `registry_source` (`lib/src/fortal`) as the analyzed authoring and
Fortal specification and test surface. Consumer applications do not depend on that package.

## Installation

The commands below apply after the first CLI release and Remix beta.11.
For checkout use, follow the [Open Code installation instructions](/open-code#install).

```bash
flutter pub add dev:remix_cli
dart run remix_cli:remix init --prefix Fortal --preset fortal
dart run remix_cli:remix add button
```

The `Fortal` prefix keeps the API names shown throughout this guide. Add each
additional item when you use it, and import your installed `lib/ui/ui.dart`
barrel from application code.

## Quick Start

Wrap your app with `FortalTheme` to provide the design tokens, then use the
`Fortal*` widgets. Named constructors select a fixed variant; use the unnamed
constructor with `variant:` when the choice is dynamic:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FortalTheme(
      child: WidgetsApp(
        color: Colors.white,
        builder: (_, _) => Center(
          child: FortalButton.solid(
            onPressed: () {},
            label: 'Fortal Button',
          ),
        ),
      ),
    );
  }
}
```

`FortalTheme` is a `MixScope`, so it satisfies the theme-scope row of Remix's
[host capabilities](/#host-capabilities) table. Place it above your app widget so
that overlay and route content inherits the tokens.

## Scope placement

The outermost `FortalTheme` provides design tokens and sets default text and icon
styles for standard Flutter widgets (`DefaultTextStyle` and `IconTheme` using
`gray12`). Unstyled `Text` widgets receive the `text3` size (16px, 1.5 line
height) in regular weight with `gray12`. Fortal components (`FortalText`,
`FortalCode`, `FortalKbd`, and `FortalLink`) resolve their styling directly from
active theme tokens instead.

A nested scope updates design tokens for its subtree. When the nested scope
changes `brightness` or `gray`, it updates `DefaultTextStyle` and `IconTheme` to
the new `gray12` color while preserving existing font metrics.

Use `WidgetsApp` for the application host. For an application with routes, put
`FortalTheme` in `builder`, above the Navigator, so pushed routes and dialogs
inherit the same tokens and text style:

```dart
import 'package:flutter/widgets.dart';
import 'ui/ui.dart';

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

  @override
  Widget build(BuildContext context) {
    return WidgetsApp(
      color: const Color(0xFFF8FAFC),
      pageRouteBuilder: <T>(settings, builder) => PageRouteBuilder<T>(
          settings: settings,
          pageBuilder: (context, animation, secondaryAnimation) => builder(context),
        ),
      builder: (context, child) => FortalTheme(child: child!),
      home: const Center(child: FortalText('Themed')),
    );
  }
}
```

For app-wide toasts, wrap the Navigator in `Overlay.wrap` and
`RemixToastScope` inside this builder, below `FortalTheme`. See the
[toast scope example](/components/toast).

The root run is only a fallback for bare Flutter `Text`, and it follows
Flutter's normal text-style cascade. A nearer `DefaultTextStyle`, including one
installed by `Material` or `Scaffold`, wins for that bare text. Fortal
typography does not inherit it: omitted sizes use the `text3` token, and an
explicit `size:` selects another token size. Transparent, non-accent
`FortalCode.ghost` is the deliberate exception for foreground only, so it can
blend into surrounding text without inheriting that run's metrics.

<Warning>
  If an overlay's text does not inherit your theme, ensure the theme scope wraps
  the Navigator or Overlay that owns the entry. A scope inside one route cannot
  theme entries inserted above that route.
</Warning>

## Light, dark, and system appearance

`FortalTheme` supplies light and dark preset defaults and follows the system at
the root. Nested scopes inherit the configured pair and active selection.
Customize the pair with `theme` and `darkTheme`, or force `mode` to `.light` or
`.dark`. `mode: .system` follows live platform brightness changes.

```dart
import 'package:flutter/widgets.dart';
import 'ui/ui.dart';

Widget withAppearance(Widget child) => FortalTheme(
  theme: const FortalThemeConfig(
    accent: FortalAccentColor.calcadao,
    brightness: Brightness.light,
  ),
  darkTheme: const FortalThemeConfig(
    accent: FortalAccentColor.calcadao,
    brightness: Brightness.dark,
  ),
  mode: FortalThemeMode.system,
  child: child,
);
```

Supplying only `theme` makes it the fallback for both modes. Supplying only
`darkTheme` retains the default or inherited base theme. Direct scope options
such as `accent` apply to both appearances. Appearance selection uses `mode`;
the scope no longer accepts a `brightness` parameter.

For routed WidgetsApp examples, wrap the Navigator with the scope in `builder`.
A default WidgetsApp also preserves the text defaults of a scope above it. An
explicit WidgetsApp `textStyle` overrides those outer defaults; a scope inside
the builder establishes its defaults below the app’s text style.

Installed Fortal source uses your prefix: `AcmeTheme`, `AcmeThemeData`, and
`AcmeThemeMode` expose the same behavior. No Material application host is needed.

## Customizing Fortal Styles

Fortal widgets call the matching `fortal*Style` recipe internally. Use those
recipes directly when you need a custom Remix widget composition:

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

final style = fortalButtonStyle(variant: FortalButtonVariant.solid)
  .borderRadius(.all(const Radius.circular(8)))
  .padding(.horizontal(32))
  .onHovered(.scale(1.05));
```

## Fortal Design Tokens

Fortal styles are built on a robust token system that includes:

- **Colors**: 12-step accent and gray scales (generated in OKLCH from owned seeds)
- **Spacing**: 9-step spacing scale
- **Border Radius**: 6-step radius scale
- **Shadows**: 6-level shadow system
- **Typography**: 9-size type scale
- **Border Widths**: Consistent stroke weights

You can use these tokens directly in your custom styles:

```dart
import 'package:remix/remix.dart';
import 'ui/ui.dart';

final style = ButtonStyler()
  .color(FortalTokens.accent9())
  .padding(.all(FortalTokens.space4()))
  .borderRadius(.all(FortalTokens.radius3()))
  .label(TextStyler().color(FortalTokens.accentContrast()));
```

## Typography

Fortal adds five typography families that base Remix has no counterpart for:
`FortalText`, `FortalHeading`, `FortalCode`, `FortalKbd`, and `FortalLink`. They
share one nine-step `FortalTextSize` scale and one `FortalTextWeight` enum. See
[Typography](/fortal/typography) for widget selection, controls, semantics,
recipe customization, and composition guidance.

## Charts

Fortal provides themed line, bar, and pie recipes backed by `mix_chart`. Install
the owned chart recipe through the same configured preset:

```bash
dart run remix_cli:remix add chart
```

```dart
import 'package:mix_chart/mix_chart.dart';
import 'ui/ui.dart';

final chart = FortalLineChart(
  semanticsLabel: 'Weekly revenue',
  series: [
    LineSeries(
      id: 'revenue',
      label: 'Revenue',
      points: [
        ChartPoint(id: 'mon', x: 0, y: 18),
        ChartPoint(id: 'tue', x: 1, y: 31),
      ],
    ),
  ],
);
```

Use `FortalLineChart`, `FortalBarChart`, and `FortalPieChart` for the ready-made
widgets. Their matching `fortal*ChartStyle()` recipes can style the underlying
`mix_chart` widgets directly.

## Icons

Install the optional application-owned icon aliases through the configured
preset:

```bash
dart run remix_cli:remix add icons
```

That item adds `remix_ui_icons` and creates `FortalIcons` with the small alias
set used by your UI layer. Add, rename, or remove those aliases as the
application vocabulary evolves. Import
`package:remix_ui_icons/remix_ui_icons.dart` and use `RemixIcons` directly when
you need the complete 318-glyph Radix Icons 1.3.2 catalog.

Catalogs, galleries, and drift tests that must enumerate every glyph can import
the opt-in index. An application that never imports it keeps full font
subsetting:

```dart
import 'package:remix_ui_icons/icons_index.dart';

final icon = remixIconsByName['check'];
```

## Seven palettes

Both the default accent and the sole gray family are **Calçadão**: basalt and
limestone neutrals. The page uses neutral step 1 and solid panels use step 2
in both modes. Choose a named accent with `FortalTheme(accent: .mar, child: child)`.

| Palette | Enum | Seed | Solid foreground |
| --- | --- | --- | --- |
| Calçadão | `calcadao` | `#13181D` | Limestone / basalt by mode |
| Mar | `mar` | `#036F6B` | White or basalt, contrast-fitted |
| Céu | `ceu` | `#006EBE` | White or basalt, contrast-fitted |
| Mangue | `mangue` | `#1D7D3E` | White or basalt, contrast-fitted |
| Sol | `sol` | `#FFCF50` | Basalt |
| Duna | `duna` | `#E5C79A` | Basalt |
| Caju | `caju` | `#CB4333` | White or basalt, contrast-fitted |

Seeds identify each palette; the generator fits final colors to sRGB and the
contrast contract. Background, border, and text steps share OKLCH lightness
stops; seed-led solid steps 9–10 preserve each palette's identity. Transparent
steps are solved against the neutral page. Their appearance on a different
background follows ordinary alpha compositing.

| Steps | Job |
| --- | --- |
| 1–2 | Page and subtle backgrounds |
| 3–5 | Component fills: rest, hover, pressed |
| 6–8 | Borders: subtle, normal, strong; step 8 also supplies focus |
| 9–10 | Solid fill and hover |
| 11–12 | Secondary and primary text |

The `contrast`, `surface`, `indicator`, and `track` roles remain available.
Error tokens always use Caju, independently of the selected accent.

## Contrast and geometry

Every built-in palette is tested in both modes:

- Contrast text on steps 9 and 10: **at least 4.5:1**.
- Step 11 on steps 1–3: **at least 4.5:1**.
- Step 12 on steps 1–5: **at least 7:1**.
- Step 8 on steps 1 and 2: **at least 3:1**.
- Actual soft alpha labels on fills 3–5 over the page and solid panel: **at least 4.5:1**.

Soft badges, callouts, code, avatars, and buttons are readable by default.
`highContrast` remains available to strengthen their foreground treatment.
Interactive controls share a token-driven 2px step-8 focus ring with a 2px
outside gap. Compact controls keep their painted sizes within interaction
areas of at least 40 logical pixels.

Panels default to `solid`, with zero blur. Opt into `panelBackground:
.translucent` for translucent panels. Only floating menu, popover, dialog,
select-popup, tooltip, and toast layers carry elevation; cards and controls
are flat. Surface fields and select triggers use opaque gray step 2 with a
step-7 border. Radius, scaling, high contrast, font selection, and icons remain
independent options.

## Your own brand seed

The palette tool has no external color-data dependency. From a Remix checkout,
generate a palette next to the installed `palette.dart`:

```bash
fvm dart run registry_source/tool/fortal_palettes/generate.dart \
  --name brand --seed '#8038CC' --type-prefix Fortal \
  --output /path/to/app/lib/ui/theme/brand_colors.dart
```

Set `--type-prefix` to the prefix in the app's `remix.yaml` (for example,
`Acme`). The default is `Fortal`. `--palette-import` changes the sibling import
when the file lives elsewhere. Import the generated file in the app and pass
`accentPalette: brand` to its theme. A custom palette supplies both modes and
inherits through nested scopes; an explicit named `accent` in a nested scope
selects that built-in palette again.

## Owned specification and review

The [catalog](/fortal/catalog) is generated from Fortal's own machine-readable
specification, checked against its source APIs and test inventory in CI. The
[prose specification](https://github.com/btwld/remix/blob/main/registry_source/specs/fortal.md)
owns tokens, defaults, geometry, variants, and contrast targets.

The Fortal playground in `apps/demo` includes a polish board with every family,
every size and variant, side-by-side light/dark panels, palette selection,
a 4px grid, and hover/pressed/focus/disabled review states:

```bash
cd apps/demo
fvm flutter run -d chrome -t lib/polish_main.dart
```

Owned golden tests capture the board and all seven palette/state combinations.
For usage examples, see the `Fortal widgets` section on each
[component page](/components/button).
