Design Tokens

Typography

Create and manage typography design tokens with CSS variables for consistent text styling across your application.

Overview

The typography composables help you create comprehensive type systems with minimal code. They generate typography-related variables that can be easily referenced throughout your application, enabling flexible theming and consistent text styling across different components and contexts.

Why use typography composables?

Typography composables help you:

  • Centralize typography definitions: Define all your font families, sizes, weights, line heights, and letter spacing in one place.
  • Enable flexible theming: Override typography values to instantly update text styles across your application.
  • Maintain consistency: Use semantic names to ensure consistent typography usage throughout your design system.
  • Create harmonious scales: Integrate with modular scales to generate mathematically proportional type systems.
  • Simplify maintenance: Update typography values in one place instead of searching through your codebase.

useFontFamily

The useFontFamily() function creates a set of font family variables from a simple object of font stack definitions.

Default Font Family Values

Styleframe provides carefully chosen default font family values that you can use out of the box:

import { styleframe } from 'styleframe';
import { useFontFamily } from '@styleframe/theme';

const s = styleframe();

const {
    fontFamily,
    fontFamilyBase,
    fontFamilyPrint,
    fontFamilyMono,
} = useFontFamily(s);

export default s;

The default values include:

  • base: System font stack for optimal performance and native appearance
  • print: Serif font stack for print media and article content
  • mono: Monospace font stack for code and technical content
  • default: References base by default
Pro tip: Use the default key for your primary font family. It will create a variable named --font-family without any suffix, making it the natural choice for body text and general use.

Creating Custom Font Family Variables

You can provide completely custom font family values:

import { styleframe } from 'styleframe';
import { useFontFamily } from '@styleframe/theme';

const s = styleframe();

const {
    fontFamily,
    fontFamilyMono,
    fontFamilySerif,
} = useFontFamily(s, {
    default: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif",
    mono: "'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
    serif: "'Georgia', 'Times New Roman', 'Times', serif",
} as const);

export default s;

Extending the Default Font Family Values

You can extend the defaults with additional custom font families:

import { styleframe } from 'styleframe';
import { useFontFamily, defaultFontFamilyValues } from '@styleframe/theme';

const s = styleframe();

const {
    fontFamily,
    fontFamilyBase,
    fontFamilyPrint,
    fontFamilyMono,
    fontFamilyDisplay,
} = useFontFamily(s, {
    ...defaultFontFamilyValues,
    display: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
} as const);

export default s;

useFontSize

The useFontSize() function creates a set of font size variables.

import { styleframe } from 'styleframe';
import { useFontSize } from '@styleframe/theme';

const s = styleframe();

const {
    fontSize,
    fontSizeXs,
    fontSizeSm,
    fontSizeMd,
    fontSizeLg,
    fontSizeXl,
} = useFontSize(s, {
    default: '1rem',
    xs: '0.75rem',
    sm: '0.875rem',
    md: '1rem',
    lg: '1.25rem',
    xl: '1.5rem',
} as const);

export default s;

Integration with useMultiplier

The real power of useFontSize comes when combined with useMultiplier() and modular scales. This allows you to create mathematically harmonious type systems:

import { styleframe } from 'styleframe';
import { useScale, useScalePowers, useMultiplier, useFontSize } from '@styleframe/theme';

const s = styleframe();

// Use Perfect Fourth scale (1.333) for clear typographic hierarchy
const { scale } = useScale(s, 'perfect-fourth');

// Define base font size
const { fontSize } = useFontSize(s, { default: '1rem' });

// Create scale powers
const scalePowers = useScalePowers(s, scale, [-2, -1, 0, 1, 2, 3, 4, 5]);

// Generate font size scale automatically
const {
    fontSizeXs,
    fontSizeSm,
    fontSizeMd,
    fontSizeLg,
    fontSizeXl,
    fontSize2xl,
    fontSize3xl,
    fontSize4xl,
} = useMultiplier(s, fontSize, {
    xs: scalePowers[-2],   // ~0.56rem
    sm: scalePowers[-1],   // ~0.75rem
    md: scalePowers[0],    // 1rem (base)
    lg: scalePowers[1],    // ~1.33rem
    xl: scalePowers[2],    // ~1.78rem
    '2xl': scalePowers[3], // ~2.37rem
    '3xl': scalePowers[4], // ~3.16rem
    '4xl': scalePowers[5], // ~4.21rem
});

export default s;

The useMultiplier() function multiplies your base font size by the scale powers, creating a harmonious progression of sizes that maintain consistent proportional relationships.

Read more about design scales and take advantage of the flexibility they offer.

Pro tip: Using useMultiplier() with scales means you can change your entire type system's proportions by simply adjusting the scale ratio. Try different scales like Major Third (1.25) for more compact hierarchies or Perfect Fifth (1.5) for dramatic size differences.

useFontWeight

The useFontWeight() function creates a set of font weight variables covering the standard range of weights.

Styleframe provides a comprehensive set of default font weight values:

import { styleframe } from 'styleframe';
import { useFontWeight } from '@styleframe/theme';

const s = styleframe();

const {
    fontWeight,
    fontWeightExtralight,
    fontWeightLight,
    fontWeightNormal,
    fontWeightMedium,
    fontWeightSemibold,
    fontWeightBold,
    fontWeightBlack,
    fontWeightLighter,
    fontWeightBolder,
    fontWeightInherit,
} = useFontWeight(s);

export default s;

The default values include:

  • extralight: 200 - Ultra thin weight
  • light: 300 - Light weight
  • normal: normal keyword (typically 400)
  • medium: 500 - Medium weight
  • semibold: 600 - Semibold weight
  • bold: bold keyword (typically 700)
  • black: 900 - Heaviest weight
  • lighter: lighter keyword (relative to parent)
  • bolder: bolder keyword (relative to parent)
  • inherit: Inherits from parent
  • default: References normal by default

Creating Custom Font Weight Variables

Override font weights to match your font's available weights:

import { styleframe } from 'styleframe';
import { useFontWeight } from '@styleframe/theme';

const s = styleframe();

const {
    fontWeight,
    fontWeightLight,
    fontWeightRegular,
    fontWeightMedium,
    fontWeightBold,
} = useFontWeight(s, {
    default: 400,
    light: 300,
    regular: 400,
    medium: 500,
    bold: 700,
} as const);

export default s;

Extending the Default Font Weight Values

You can keep the defaults and override specific values:

import { styleframe } from 'styleframe';
import { useFontWeight, defaultFontWeightValues } from '@styleframe/theme';

const s = styleframe();

const { fontWeight } = useFontWeight(s, {
    ...defaultFontWeightValues,
    default: '@semibold'
} as const);

export default s;

useFontStyle

The useFontStyle() function creates a set of font style variables for controlling text styling like italic and oblique.

Styleframe provides all standard CSS font style values:

import { styleframe } from 'styleframe';
import { useFontStyle } from 'styleframe/theme';

const s = styleframe();

const {
    fontStyle,
    fontStyleItalic,
    fontStyleOblique,
    fontStyleNormal,
    fontStyleInherit,
} = useFontStyle(s);

export default s;

The default values include:

  • italic: Italic style (uses true italic font if available)
  • oblique: Oblique style (slanted version of normal font)
  • normal: Normal upright style
  • inherit: Inherits from parent
  • default: References normal by default
Good to know:italic uses a true italic font variant if available, while oblique artificially slants the font. Most fonts provide italic variants, making italic the preferred choice for emphasis.

Creating Custom Font Style Variables

Define custom font style values for specific needs:

import { styleframe } from 'styleframe';
import { useFontStyle } from 'styleframe/theme';

const s = styleframe();

const {
    fontStyle,
    fontStyleItalic,
    fontStyleSlanted,
} = useFontStyle(s, {
    default: 'normal',
    italic: 'italic',
    slanted: 'oblique 15deg',
} as const);

export default s;

Extending the Default Font Style Values

You can extend the defaults with additional custom values:

import { styleframe } from 'styleframe';
import { useFontStyle, defaultFontStyleValues } from 'styleframe/theme';

const s = styleframe();

const { fontStyle } = useFontStyle(s, {
    ...defaultFontStyleValues,
    default: '@italic'
} as const);

export default s;

Updating the Default Font Style Variable

You can override the default font style value after creating it:

import { styleframe } from 'styleframe';
import { useFontStyle } from 'styleframe/theme';

const s = styleframe();
const { variable } = s;

const { fontStyle } = useFontStyle(s);

// Override the default font style
variable(fontStyle, 'italic');

export default s;

useLineHeight

The useLineHeight() function creates a set of line height variables for controlling vertical rhythm and text readability.

Styleframe provides carefully balanced default line height values:

import { styleframe } from 'styleframe';
import { useLineHeight } from '@styleframe/theme';

const s = styleframe();

const {
    lineHeight,
    lineHeightTight,
    lineHeightSnug,
    lineHeightNormal,
    lineHeightRelaxed,
    lineHeightLoose,
} = useLineHeight(s);

export default s;

The default values include:

  • tight: 1.2 - For headings and display text
  • snug: 1.35 - For compact UI text
  • normal: 1.5 - For body text (optimal readability)
  • relaxed: 1.65 - For longer reading passages
  • loose: 1.9 - For maximum spacing and accessibility
  • default: References normal by default
Good to know: Line height values are unitless multipliers. A line height of 1.5 means 1.5 times the font size, which scales proportionally as font sizes change.

Creating Custom Line Height Variables

Define custom line heights for specific design needs:

import { styleframe } from 'styleframe';
import { useLineHeight } from '@styleframe/theme';

const s = styleframe();

const {
    lineHeight,
    lineHeightHeading,
    lineHeightBody,
    lineHeightArticle,
} = useLineHeight(s, {
    default: 1.5,
    heading: 1.2,
    body: 1.5,
    article: 1.75,
} as const);

export default s;

Updating the Default Line Height Variable

You can override the default line height value after creating it:

import { styleframe } from 'styleframe';
import { useLineHeight } from '@styleframe/theme';

const s = styleframe();
const { variable } = s;

const { lineHeight } = useLineHeight(s);

// Override the default line height
variable(lineHeight, 1.65);

export default s;

useLetterSpacing

The useLetterSpacing() function creates a set of letter spacing (tracking) variables for fine-tuning text appearance.

Styleframe provides balanced default letter spacing values:

import { styleframe } from 'styleframe';
import { useLetterSpacing } from '@styleframe/theme';

const s = styleframe();

const {
    letterSpacing,
    letterSpacingTighter,
    letterSpacingTight,
    letterSpacingNormal,
    letterSpacingWide,
    letterSpacingWider,
} = useLetterSpacing(s);

export default s;

The default values include:

  • tighter: -0.05em - Very tight spacing for large display text
  • tight: -0.025em - Tight spacing for headings
  • normal: normal keyword - Default browser spacing
  • wide: 0.05em - Loose spacing for small text or all-caps
  • wider: 0.1em - Very loose spacing for labels and UI text
  • default: References normal by default
Good to know: Letter spacing uses em units so it scales proportionally with font size. Negative values tighten spacing, positive values loosen it.

Creating Custom Letter Spacing Variables

Define custom letter spacing for specific typography styles:

import { styleframe } from 'styleframe';
import { useLetterSpacing } from '@styleframe/theme';

const s = styleframe();

const {
    letterSpacing,
    letterSpacingHeading,
    letterSpacingBody,
    letterSpacingLabel,
} = useLetterSpacing(s, {
    default: 'normal',
    heading: '-0.02em',
    body: 'normal',
    label: '0.08em',
} as const);

export default s;

Extending the Default Letter Spacing Values

You can extend the defaults with additional custom values:

import { styleframe } from 'styleframe';
import { useLetterSpacing, defaultLetterSpacingValues } from '@styleframe/theme';

const s = styleframe();

const {
    letterSpacing,
    letterSpacingTighter,
    letterSpacingTight,
    letterSpacingNormal,
    letterSpacingWide,
    letterSpacingWider,
    letterSpacingWidest,
} = useLetterSpacing(s, {
    ...defaultLetterSpacingValues,
    widest: '0.15em',
} as const);

export default s;

Using Typography Variables

Once created, typography variables can be combined to create complete text styles:

import { styleframe } from 'styleframe';
import { useFontFamily, useFontSize, useFontWeight, useLineHeight, useLetterSpacing } from '@styleframe/theme';

const s = styleframe();
const { ref, selector } = s;

// Define typography variables
const { fontFamily, fontFamilyMono } = useFontFamily(s);
const { fontSizeXl, fontSize2xl, fontSize3xl } = useFontSize(s, {
    xl: '1.25rem',
    '2xl': '1.5rem',
    '3xl': '2rem',
} as const);
const { fontWeightNormal, fontWeightSemibold, fontWeightBold } = useFontWeight(s);
const { fontStyleNormal, fontStyleItalic } = useFontStyle(s);
const { lineHeightTight, lineHeightNormal } = useLineHeight(s);
const { letterSpacingTight, letterSpacingWide } = useLetterSpacing(s);

// Apply to elements
selector('h1', {
    fontFamily: ref(fontFamily),
    fontSize: ref(fontSize3xl),
    fontWeight: ref(fontWeightBold),
    fontStyle: ref(fontStyleNormal),
    lineHeight: ref(lineHeightTight),
    letterSpacing: ref(letterSpacingTight),
});

selector('body', {
    fontFamily: ref(fontFamily),
    fontWeight: ref(fontWeightNormal),
    fontStyle: ref(fontStyleNormal),
    lineHeight: ref(lineHeightNormal),
});

selector('em, i', {
    fontStyle: ref(fontStyleItalic),
});

selector('code', {
    fontFamily: ref(fontFamilyMono),
    fontSize: ref(fontSizeXl),
    letterSpacing: ref(letterSpacingWide),
});

export default s;

Examples

Complete Typography System

Here's a comprehensive example showing a full typography system with all composables working together:

import { styleframe } from 'styleframe';
import { 
    useFontFamily,
    useFontSize,
    useFontWeight,
    useLineHeight,
    useLetterSpacing,
    useScale,
    useScalePowers,
    useMultiplier
} from '@styleframe/theme';

const s = styleframe();
const { ref, selector } = s;

// Font families
const { fontFamily, fontFamilyMono, fontFamilyDisplay } = useFontFamily(s, {
    default: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
    mono: "'JetBrains Mono', 'Fira Code', monospace",
    display: "'Inter', sans-serif",
} as const);

// Font size with scale
const { scale } = useScale(s, 'perfect-fourth');
const { fontSize } = useFontSize(s, { default: '1rem' });
const scalePowers = useScalePowers(s, scale, [-2, -1, 0, 1, 2, 3, 4]);

const {
    fontSizeXs,
    fontSizeSm,
    fontSizeMd,
    fontSizeLg,
    fontSizeXl,
    fontSize2xl,
    fontSize3xl,
} = useMultiplier(s, fontSize, {
    xs: scalePowers[-2],
    sm: scalePowers[-1],
    md: scalePowers[0],
    lg: scalePowers[1],
    xl: scalePowers[2],
    '2xl': scalePowers[3],
    '3xl': scalePowers[4],
});

// Font weights
const { fontWeightNormal, fontWeightMedium, fontWeightSemibold, fontWeightBold } = useFontWeight(s);

// Font styles
const { fontStyleNormal, fontStyleItalic } = useFontStyle(s);

// Line heights
const { lineHeightTight, lineHeightNormal, lineHeightRelaxed } = useLineHeight(s);

// Letter spacing
const { letterSpacingTight, letterSpacingNormal, letterSpacingWide } = useLetterSpacing(s);

// Apply to elements
selector('body', {
    fontFamily: ref(fontFamily),
    fontSize: ref(fontSizeMd),
    fontWeight: ref(fontWeightNormal),
    lineHeight: ref(lineHeightNormal),
    letterSpacing: ref(letterSpacingNormal),
});

selector('h1', {
    fontFamily: ref(fontFamilyDisplay),
    fontSize: ref(fontSize3xl),
    fontWeight: ref(fontWeightBold),
    lineHeight: ref(lineHeightTight),
    letterSpacing: ref(letterSpacingTight),
});

selector('h2', {
    fontFamily: ref(fontFamilyDisplay),
    fontSize: ref(fontSize2xl),
    fontWeight: ref(fontWeightBold),
    lineHeight: ref(lineHeightTight),
    letterSpacing: ref(letterSpacingTight),
});

selector('h3', {
    fontFamily: ref(fontFamilyDisplay),
    fontSize: ref(fontSizeXl),
    fontWeight: ref(fontWeightSemibold),
    lineHeight: ref(lineHeightTight),
});

selector('p', {
    fontSize: ref(fontSizeMd),
    lineHeight: ref(lineHeightRelaxed),
});

selector('small', {
    fontSize: ref(fontSizeSm),
    lineHeight: ref(lineHeightNormal),
});

selector('code', {
    fontFamily: ref(fontFamilyMono),
    fontSize: ref(fontSizeSm),
    letterSpacing: ref(letterSpacingWide),
});

selector('.lead', {
    fontSize: ref(fontSizeLg),
    fontWeight: ref(fontWeightMedium),
    lineHeight: ref(lineHeightRelaxed),
});

selector('.label', {
    fontSize: ref(fontSizeXs),
    fontWeight: ref(fontWeightMedium),
    textTransform: 'uppercase',
    letterSpacing: ref(letterSpacingWide),
});

export default s;

Responsive Typography

Adjust typography variables at different breakpoints for optimal readability:

import { styleframe } from 'styleframe';
import { useFontSize, useLineHeight } from '@styleframe/theme';

const s = styleframe();
const { ref, selector, variable, media } = s;

const { fontSize } = useFontSize(s, { default: '1rem' });
const { lineHeight } = useLineHeight(s);

selector('body', {
    fontSize: ref(fontSize),
    lineHeight: ref(lineHeight),
});

// Increase base font size on larger screens
selector('body', ({ media }) => {
    media('(min-width: 768px)', ({ variable }) => {
        variable(fontSize, '1.125rem');
    });
});

selector('body', ({ media }) => {
    media('(min-width: 1200px)', ({ variable }) => {
        variable(fontSize, '1.25rem');
    });
});

export default s;

Best Practices

  • Use modular scales for font sizes: Combine useFontSize() with useMultiplier() for mathematically harmonious type scales.
  • Limit your type scale: Aim for 6-8 font sizes. Too many options lead to inconsistency.
  • Match line height to font size: Larger text needs tighter line height (1.2), body text works well at 1.5, and small text may need looser spacing (1.75).
  • Use negative letter spacing for large headings: Display text benefits from tighter tracking (e.g., -0.025em).
  • Use positive letter spacing for uppercase or small text: All-caps text and small UI text need extra tracking (e.g., 0.05em to 0.1em) for readability.
  • Always include font stack fallbacks: List fonts from most specific to most generic, ending with a generic family.
    font-family: "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    
  • Use local() in @font-face: Include local() sources to reuse installed fonts when available, reducing bandwidth.
  • Consider performance:
    • System fonts load instantly; web fonts may cause layout shifts. Use font-display: swap.
    • Preload critical web fonts: <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
    • Use preconnect for external font services: <link rel="preconnect" href="https://fonts.googleapis.com">
    • Use font-size-adjust or size-adjust (in @font-face) to reduce reflow when fallback fonts render.
  • Use unitless line heights: This ensures line height scales proportionally with font size changes.
  • Consider variable fonts: Variable fonts offer multiple weights and styles in a single file, improving performance and enabling optical size adjustments.
  • Test across devices: Ensure your typography is readable at all viewport sizes and on different devices.
  • Use em units for letter spacing: This ensures tracking scales proportionally with font size.
Good to know: We use as const to ensure the object is treated as a constant type. This helps TypeScript infer the return type of the composables and provides better type safety and autocomplete support.

FAQ