Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Components & templates
  2. Global foundation and fonts

Global foundation and fonts#

An app built on @craft-ts/style has no styles.css. What used to go there falls into three layers, and craft-ts writes the first two for you:

layerwritten bywhat it holds
craft.resetcraft-ts, on by defaulta modern reset
craft.basecraft-ts, on by defaultcolour scheme, focus ring, reduced motion, selection, form accent
craft.globalyour app, craftGlobalStylesyour theme variables on :root, element defaults (body, a, …)

They come before the component layers. The full order is fixed by the emitter, whatever order your modules are imported in:

css
@layer craft.reset, craft.base, craft.tokens, craft.global,
       craft.components, craft.variants, craft.overrides;

Every layer is named craft.*. A third-party stylesheet that arrives unlayered wins over all of them — that is how CSS treats unlayered styles — which is exactly why one should be rare, deliberate and attested.

The reset#

On by default. It sets box-sizing: border-box everywhere, removes default margins, makes media blocks that never overflow (max-inline-size: 100%), lets form controls inherit the document font, balances headings and avoids orphans in paragraphs (text-wrap), wraps long words (overflow-wrap: anywhere) and stops mobile browsers from inflating text.

It is written with the typed vocabulary, like any sheet (libs/style/src/lib/global/reset.ts). Turning it off is a deliberate choice:

ts
craftStyle({ reset: false });

The base#

Also on by default (base: false to opt out). It states once, for the whole document, what components used to have to remember one by one:

  • color-scheme follows the scheme axis, so scrollbars and form controls turn dark with the page;

  • every :focus-visible gets a visible ring;

  • scrolling is smooth only for users who did not ask for less motion, and under prefers-reduced-motion every animation and transition collapses to an instant. The guard is !important in the earliest layer, the one place that beats every later layer, so no component can forget it;

  • accent-color and ::selection come from the theme.

The colours and sizes are typed theme variables, exported as craftBase: accent, focusRing, focusWidth, focusOffset, selectionBg, selectionInk. Re-theme them from your own global styles (below).

Your app's globals#

ts
import {
  color,
  craftBase,
  craftGlobalStyles,
  darkOf,
  definePalette,
  fontFamily,
  scheme,
  set,
  when,
} from '@craft-ts/style';

export const brand = definePalette('brand', {
  accent: { primary: { light: '#1b5fa1', dark: '#6fb2f0' } },
  text: { strong: { light: '#111318', dark: '#f2f4f8' } },
});

craftGlobalStyles('app', {
  // The foundation's focus ring and form accent, in the app's colours.
  root: [
    set(craftBase.focusRing, brand.accent.primary),
    set(craftBase.accent, brand.accent.primary),
    when(scheme.dark, [
      set(craftBase.focusRing, darkOf(brand.accent.primary)),
      set(craftBase.accent, darkOf(brand.accent.primary)),
    ]),
  ],
  elements: {
    body: [fontFamily(bodyFont), color(brand.text.strong)],
  },
});

root goes on :root, elements on tag names — the only selectors accepted. Anything narrower than an element belongs to a component sheet. The items are the same as in a sheet: generated properties, set(...), when(...), and pseudo.*. when on a state axis becomes an attribute on :root, which is how a theme toggle is written (when(theme.dark, [...]) → :root[data-theme='dark']).

no-raw-css-value applies here as everywhere else: no literal reaches a helper.

Fonts#

ts
import { defineFont, googleFont } from '@craft-ts/style';

export const bodyFont = defineFont('body', {
  family: 'Chivo',
  source: googleFont({ weights: [400, 600, 700] }),
  display: 'swap',
  fallback: 'system-ui',
});

defineFont replaces the three things a styles.css used to carry:

  • the @import url(fonts.googleapis…) — the plugin injects a preconnect to both Google origins, then the stylesheet preloaded and applied, into <head> of index.html. An @import inside the CSS could only be discovered once the CSS itself had downloaded;

  • the @font-face blocks — localFont({ files: [...] }) emits them, and preloads the .woff2 files;

  • the * { font-family: … !important } — the returned token is a family stack, used once with fontFamily(bodyFont) on body and inherited from there. Form controls inherit it through the reset.

A server renderer that writes <head> itself reads the same tags as HTML:

ts
import head from 'virtual:craft-style-head';

Fallback without layout shift#

While the web font loads, the fallback font is shown, and the text jumps when the real one arrives. adjustFallback builds a "<family> Fallback" face from a local font, resized so both occupy the same space:

ts
defineFont('body', {
  family: 'Chivo',
  source: googleFont({ weights: [400, 700] }),
  fallback: 'system-ui',
  adjustFallback: {
    // The web font's metrics, in font units. Capsize publishes them for every
    // Google font (@capsizecss/metrics).
    metrics: {
      unitsPerEm: 1000,
      ascent: 954,
      descent: -250,
      lineGap: 0,
      xWidthAvg: 505,
    },
    // Optional, Arial by default: `face: { local: 'Helvetica', metrics }`.
  },
});

size-adjust matches the average glyph width, so lines break at the same place; ascent-override, descent-override and line-gap-override keep the line height.

←Tokens and variablesPseudo-elements and animations→

On this page

  • The reset
  • The base
  • Your app's globals
  • Fonts
  • Fallback without layout shift

@craft-ts · MIT licensed

GitHubnpm

Search