Skip to main content

Theming

Drift provides a Material Design 3 inspired theming system.

Using Theme

Query only the parts you need with granular accessors:

func (s *myState) Build(ctx core.BuildContext) core.Widget {
colors := theme.ColorsOf(ctx)
textTheme := theme.TextThemeOf(ctx)

return widgets.Container{
Color: colors.Surface,
Child: widgets.Text{Content: "Hello", Style: textTheme.HeadlineLarge},
}
}

Available Accessors

colors := theme.ColorsOf(ctx)
textTheme := theme.TextThemeOf(ctx)
themeData := theme.ThemeOf(ctx)

Providing Theme

Wrap your app with a Theme widget:

theme.Theme{
Data: theme.DefaultDarkTheme(), // or DefaultLightTheme()
Child: myApp,
}

Built-in Themes

Drift includes light and dark themes:

// Light theme
theme.DefaultLightTheme()

// Dark theme
theme.DefaultDarkTheme()

Color Scheme

The color scheme follows Material Design 3:

ColorPurpose
PrimaryMain brand color
OnPrimaryText/icons on primary
PrimaryContainerTonal container for primary
OnPrimaryContainerText/icons on primary container
SecondarySupporting brand color
OnSecondaryText/icons on secondary
SecondaryContainerTonal container for secondary
OnSecondaryContainerText/icons on secondary container
TertiaryAccent color for contrast
OnTertiaryText/icons on tertiary
TertiaryContainerTonal container for tertiary
OnTertiaryContainerText/icons on tertiary container
SurfaceBackground for cards, sheets, menus
OnSurfaceText/icons on surface
SurfaceVariantAlternative surface color
OnSurfaceVariantText/icons on surface variant
BackgroundApp background
OnBackgroundText/icons on background
ErrorError states
OnErrorText/icons on error
ErrorContainerTonal container for error
OnErrorContainerText/icons on error container
OutlineBorders and dividers
OutlineVariantSubtle outline for decorative elements
ShadowElevation shadows
ScrimModal barriers
InverseSurfaceSurface for inverse contexts
OnInverseSurfaceText/icons on inverse surface
InversePrimaryPrimary color for inverse contexts

Usage:

colors := theme.ColorsOf(ctx)

widgets.Container{
Color: colors.Surface,
Child: child,
}

widgets.Text{Content: "Error!", Style: graphics.TextStyle{
Color: colors.Error,
}}

Text Theme

Typography follows Material Design 3:

StyleTypical Use
DisplayLargeHero text
DisplayMediumLarge headlines
DisplaySmallSection headers
HeadlineLargePage titles
HeadlineMediumSection titles
HeadlineSmallSubsection titles
TitleLargeCard titles
TitleMediumList item titles
TitleSmallCaptions
BodyLargePrimary body text
BodyMediumSecondary body text
BodySmallTertiary text
LabelLargeButton text
LabelMediumTab labels
LabelSmallChip text

Usage:

textTheme := theme.TextThemeOf(ctx)

widgets.Text{Content: "Welcome", Style: textTheme.HeadlineLarge}
widgets.Text{Content: "Body content", Style: textTheme.BodyMedium}

// Centered text using theme.TextOf (Wrap is enabled by default)
theme.TextOf(ctx, "Centered heading", textTheme.HeadlineMedium).
WithAlign(graphics.TextAlignCenter)

Text Alignment

The Align field on Text controls horizontal alignment of wrapped lines. Text wraps by default; set Wrap: graphics.TextWrapNoWrap for single-line text. Alignment only takes effect when text wraps, because unwrapped text has no paragraph width to align within.

// Left-aligned (default, wraps by default)
widgets.Text{Content: longText}

// Centered
widgets.Text{Content: longText, Align: graphics.TextAlignCenter}

// Right-aligned
widgets.Text{Content: longText, Align: graphics.TextAlignRight}

// Justified (last line is left-aligned)
widgets.Text{Content: longText, Align: graphics.TextAlignJustify}

Available alignments: TextAlignLeft (default), TextAlignRight, TextAlignCenter, TextAlignJustify, TextAlignStart, and TextAlignEnd. TextAlignStart and TextAlignEnd are direction-aware variants that currently behave like Left and Right respectively (LTR only).

Custom Themes

Create a custom theme by building ThemeData:

myTheme := theme.ThemeData{
ColorScheme: theme.ColorScheme{
Primary: graphics.RGB(0x67, 0x50, 0xA7), // Purple
OnPrimary: graphics.ColorWhite,
Secondary: graphics.RGB(0x62, 0x5B, 0x71),
OnSecondary: graphics.ColorWhite,
Surface: graphics.RGB(0xFE, 0xF7, 0xFF),
OnSurface: graphics.RGB(0x1D, 0x1B, 0x20),
Background: graphics.ColorWhite,
OnBackground: graphics.ColorBlack,
Error: graphics.RGB(0xB3, 0x26, 0x1E),
OnError: graphics.ColorWhite,
},
TextTheme: theme.DefaultTextTheme(graphics.ColorBlack),
}

theme.Theme{
Data: &myTheme,
Child: myApp,
}

Dynamic Theming

Switch themes at runtime:

type appState struct {
core.StateBase
isDark bool
}

func (s *appState) Build(ctx core.BuildContext) core.Widget {
var themeData *theme.ThemeData
if s.isDark {
themeData = theme.DefaultDarkTheme()
} else {
themeData = theme.DefaultLightTheme()
}

return theme.Theme{
Data: themeData,
Child: widgets.Column{
Children: []core.Widget{
widgets.Switch{
Value: s.isDark,
OnChanged: func(value bool) {
s.SetState(func() {
s.isDark = value
})
},
},
// Rest of your app
},
},
}
}

Nested Themes

Override theme for a subtree:

theme.Theme{
Data: theme.DefaultLightTheme(),
Child: widgets.Column{
Children: []core.Widget{
lightContent,
// Dark section within light app
theme.Theme{
Data: theme.DefaultDarkTheme(),
Child: darkSection,
},
},
},
}

Themed Widget Constructors

Most Drift widgets are explicit by default — zero values mean zero, not "use theme default." For theme-styled widgets, use the themed constructors in pkg/theme.

When using explicit widgets, you must provide the visual properties you want rendered. If you omit colors, sizes, or text styles, the widget may render transparently or with zero size. In particular, explicit TextField/TextInput, Dropdown, DatePicker, and TimePicker require you to set their colors, sizes, and text styles.

Available Constructors

ConstructorReturnsTheme Data Used
theme.TextOf(ctx, content, style)widgets.TextWrap enabled by default
theme.ButtonOf(ctx, label, onTap)widgets.ButtonButtonThemeData
theme.CheckboxOf(ctx, value, onChanged)widgets.CheckboxCheckboxThemeData
theme.DropdownOf[T](ctx, value, items, onChanged)widgets.Dropdown[T]DropdownThemeData
theme.TextFieldOf(ctx, controller)widgets.TextFieldTextFieldThemeData
theme.TextFormFieldOf(ctx)widgets.TextFormFieldTextFieldThemeData
theme.ToggleOf(ctx, value, onChanged)widgets.ToggleSwitchThemeData
theme.RadioOf[T](ctx, value, groupValue, onChanged)widgets.Radio[T]RadioThemeData
theme.TabBarOf(ctx, tabs, selectedIndex, onChanged)widgets.TabBarTabBarThemeData
theme.DatePickerOf(ctx, value, onChanged)widgets.DatePickerColorScheme
theme.TimePickerOf(ctx, hour, minute, onChanged)widgets.TimePickerColorScheme
theme.IconOf(ctx, glyph)widgets.IconColorScheme
theme.CircularProgressIndicatorOf(ctx, value)widgets.CircularProgressIndicatorColorScheme
theme.DividerOf(ctx)widgets.DividerDividerThemeData
theme.VerticalDividerOf(ctx)widgets.VerticalDividerDividerThemeData
theme.LinearProgressIndicatorOf(ctx, value)widgets.LinearProgressIndicatorColorScheme

Usage

func (s *myState) Build(ctx core.BuildContext) core.Widget {
return widgets.Column{
Children: []core.Widget{
// Themed button - reads colors, padding, etc. from theme
theme.ButtonOf(ctx, "Save", s.onSave),

widgets.VSpace(16),

// Themed checkbox
theme.CheckboxOf(ctx, s.accepted.Value(), func(v bool) {
s.accepted.Set(v)
}),

widgets.VSpace(16),

// Themed divider between sections
theme.DividerOf(ctx),

// Themed with override
theme.ButtonOf(ctx, "Custom", s.onCustom).
WithBorderRadius(0), // zero is honored
},
}
}

When to Use What

PatternWhen to Use
theme.XxxOf(ctx, ...)Most apps — consistent theme styling
Struct literalFull control over all properties
.WithX()Override specific theme values

Explicit Widgets

For widgets without themed constructors (like layout widgets), pull theme values manually:

colors := theme.ColorsOf(ctx)
textTheme := theme.TextThemeOf(ctx)

widgets.Container{
Color: colors.Surface,
Padding: layout.EdgeInsetsAll(16),
Child: widgets.Text{
Content: "Hello",
Style: textTheme.BodyLarge,
},
}

widgets.DecoratedBox{
Color: colors.SurfaceVariant,
BorderRadius: 8,
Child: content,
}

Disabled Styling

Widgets support theme-controlled disabled colors:

  • Themed widgets (theme.XxxOf) automatically use disabled colors from theme data
  • Explicit widgets without disabled colors fall back to 0.5 opacity
// Themed: uses theme disabled colors
btn := theme.ButtonOf(ctx, "Submit", onSubmit)
btn.Disabled = true // uses DisabledBackgroundColor, DisabledForegroundColor

// Explicit without disabled colors: falls back to 0.5 opacity
widgets.Button{Label: "Submit", OnTap: onSubmit, Disabled: true}

// Explicit with custom disabled colors
widgets.Button{
Label: "Submit",
Disabled: true,
Color: colors.Primary,
DisabledColor: colors.SurfaceVariant,
DisabledTextColor: colors.OnSurfaceVariant,
}

Next Steps