Skip to content

Instantly share code, notes, and snippets.

@natew
Last active March 16, 2026 22:33
Show Gist options
  • Select an option

  • Save natew/3be503cc5990a1142d17ffadf86a134f to your computer and use it in GitHub Desktop.

Select an option

Save natew/3be503cc5990a1142d17ffadf86a134f to your computer and use it in GitHub Desktop.
Code showing Tamagui ThemeBuilder generating the site theme suite
import {
blue,
blueDark,
gray,
grayDark,
green,
greenDark,
orange,
orangeDark,
pink,
pinkDark,
purple,
purpleDark,
red,
redDark,
yellow,
yellowDark,
} from '@tamagui/colors'
import { createThemeBuilder } from '@tamagui/theme-builder'
import type { Variable } from '@tamagui/web'
import { createTokens } from '@tamagui/web'
const colorTokens = {
light: {
blue,
gray,
green,
orange,
pink,
purple,
red,
yellow,
},
dark: {
blue: blueDark,
gray: grayDark,
green: greenDark,
orange: orangeDark,
pink: pinkDark,
purple: purpleDark,
red: redDark,
yellow: yellowDark,
},
}
const lightShadowColor = 'rgba(0,0,0,0.04)'
const lightShadowColorStrong = 'rgba(0,0,0,0.085)'
const darkShadowColor = 'rgba(0,0,0,0.2)'
const darkShadowColorStrong = 'rgba(0,0,0,0.3)'
const darkColors = {
...colorTokens.dark.blue,
...colorTokens.dark.gray,
...colorTokens.dark.green,
...colorTokens.dark.orange,
...colorTokens.dark.pink,
...colorTokens.dark.purple,
...colorTokens.dark.red,
...colorTokens.dark.yellow,
}
const lightColors = {
...colorTokens.light.blue,
...colorTokens.light.gray,
...colorTokens.light.green,
...colorTokens.light.orange,
...colorTokens.light.pink,
...colorTokens.light.purple,
...colorTokens.light.red,
...colorTokens.light.yellow,
}
const color = {
white0: 'rgba(255,255,255,0)',
white075: 'rgba(255,255,255,0.75)',
white05: 'rgba(255,255,255,0.5)',
white025: 'rgba(255,255,255,0.25)',
black0: 'rgba(10,10,10,0)',
black075: 'rgba(10,10,10,0.75)',
black05: 'rgba(10,10,10,0.5)',
black025: 'rgba(10,10,10,0.25)',
white1: '#fff',
white2: '#f8f8f8',
white3: 'hsl(0, 0%, 96.3%)',
white4: 'hsl(0, 0%, 94.1%)',
white5: 'hsl(0, 0%, 92.0%)',
white6: 'hsl(0, 0%, 90.0%)',
white7: 'hsl(0, 0%, 88.5%)',
white8: 'hsl(0, 0%, 81.0%)',
white9: 'hsl(0, 0%, 56.1%)',
white10: 'hsl(0, 0%, 50.3%)',
white11: 'hsl(0, 0%, 42.5%)',
white12: 'hsl(0, 0%, 9.0%)',
black1: '#050505',
black2: '#151515',
black3: '#191919',
black4: '#232323',
black5: '#282828',
black6: '#323232',
black7: '#424242',
black8: '#494949',
black9: '#545454',
black10: '#626262',
black11: '#a5a5a5',
black12: '#fff',
...postfixObjKeys(lightColors, 'Light'),
...postfixObjKeys(darkColors, 'Dark'),
}
export const palettes = (() => {
const transparent = (hsl: string, opacity = 0) =>
hsl.replace(`%)`, `%, ${opacity})`).replace(`hsl(`, `hsla(`)
const getColorPalette = (colors: Object): string[] => {
const colorPalette = Object.values(colors)
// make the transparent color vibrant and towards the middle
const colorI = colorPalette.length - 4
// add our transparent colors first/last
// and make sure the last (foreground) color is white/black rather than colorful
// this is mostly for consistency with the older theme-base
return [
transparent(colorPalette[0], 0),
transparent(colorPalette[0], 0.25),
transparent(colorPalette[0], 0.5),
transparent(colorPalette[0], 0.75),
...colorPalette,
transparent(colorPalette[colorI], 0.75),
transparent(colorPalette[colorI], 0.5),
transparent(colorPalette[colorI], 0.25),
transparent(colorPalette[colorI], 0),
]
}
const lightPalette = [
color.white0,
color.white075,
color.white05,
color.white025,
color.white1,
color.white2,
color.white3,
color.white4,
color.white5,
color.white6,
color.white7,
color.white8,
color.white9,
color.white10,
color.white11,
color.white12,
color.black075,
color.black05,
color.black025,
color.black0,
]
const darkPalette = [
color.black0,
color.black075,
color.black05,
color.black025,
color.black1,
color.black2,
color.black3,
color.black4,
color.black5,
color.black6,
color.black7,
color.black8,
color.black9,
color.black10,
color.black11,
color.black12,
color.white075,
color.white05,
color.white025,
color.white0,
]
const lightPalettes = objectFromEntries(
objectKeys(colorTokens.light).map(
(key) => [`light_${key}`, getColorPalette(colorTokens.light[key])] as const
)
)
const darkPalettes = objectFromEntries(
objectKeys(colorTokens.dark).map(
(key) => [`dark_${key}`, getColorPalette(colorTokens.dark[key])] as const
)
)
const colorPalettes = {
...lightPalettes,
...darkPalettes,
}
return {
light: lightPalette,
dark: darkPalette,
...colorPalettes,
}
})()
export const templates = (() => {
const transparencies = 3
// templates use the palette and specify index
// negative goes backwards from end so -1 is the last item
const base = {
background0: 0,
background025: 1,
background05: 2,
background075: 3,
color1: transparencies + 1,
color2: transparencies + 2,
color3: transparencies + 3,
color4: transparencies + 4,
color5: transparencies + 5,
color6: transparencies + 6,
color7: transparencies + 7,
color8: transparencies + 8,
color9: transparencies + 9,
color10: transparencies + 10,
color11: transparencies + 11,
color12: transparencies + 12,
color0: -0,
color025: -1,
color05: -2,
color075: -3,
// the background, color, etc keys here work like generics - they make it so you
// can publish components for others to use without mandating a specific color scale
// the @tamagui/button Button component looks for `$background`, so you set the
// dark_red_Button theme to have a stronger background than the dark_red theme.
background: transparencies + 1,
backgroundHover: transparencies + 2,
backgroundPress: transparencies + 3,
backgroundFocus: transparencies + 1,
borderColor: transparencies + 4,
borderColorHover: transparencies + 5,
borderColorFocus: transparencies + 2,
borderColorPress: transparencies + 4,
color: -transparencies - 1,
colorHover: -transparencies - 2,
colorPress: -transparencies - 1,
colorFocus: -transparencies - 2,
colorTransparent: -0,
placeholderColor: -transparencies - 4,
outlineColor: -1,
}
const surface1 = {
background: base.background + 1,
backgroundHover: base.backgroundHover + 1,
backgroundPress: base.backgroundPress + 1,
backgroundFocus: base.backgroundFocus + 1,
borderColor: base.borderColor + 1,
borderColorHover: base.borderColorHover + 1,
borderColorFocus: base.borderColorFocus + 1,
borderColorPress: base.borderColorPress + 1,
}
const surface2 = {
background: base.background + 2,
backgroundHover: base.backgroundHover + 2,
backgroundPress: base.backgroundPress + 2,
backgroundFocus: base.backgroundFocus + 2,
borderColor: base.borderColor + 2,
borderColorHover: base.borderColorHover + 2,
borderColorFocus: base.borderColorFocus + 2,
borderColorPress: base.borderColorPress + 2,
}
const surface3 = {
background: base.background + 3,
backgroundHover: base.backgroundHover + 3,
backgroundPress: base.backgroundPress + 3,
backgroundFocus: base.backgroundFocus + 3,
borderColor: base.borderColor + 3,
borderColorHover: base.borderColorHover + 3,
borderColorFocus: base.borderColorFocus + 3,
borderColorPress: base.borderColorPress + 3,
}
const surfaceActive = {
background: base.background + 5,
backgroundHover: base.background + 5,
backgroundPress: base.backgroundPress + 5,
backgroundFocus: base.backgroundFocus + 5,
borderColor: base.borderColor + 5,
borderColorHover: base.borderColor + 5,
borderColorFocus: base.borderColorFocus + 5,
borderColorPress: base.borderColorPress + 5,
}
const inverseSurface1 = {
color: surface1.background,
colorHover: surface1.backgroundHover,
colorPress: surface1.backgroundPress,
colorFocus: surface1.backgroundFocus,
background: base.color,
backgroundHover: base.colorHover,
backgroundPress: base.colorPress,
backgroundFocus: base.colorFocus,
borderColor: base.color - 2,
borderColorHover: base.color - 3,
borderColorFocus: base.color - 4,
borderColorPress: base.color - 5,
}
const inverseActive = {
...inverseSurface1,
background: base.color - 2,
backgroundHover: base.colorHover - 2,
backgroundPress: base.colorPress - 2,
backgroundFocus: base.colorFocus - 2,
borderColor: base.color - 2 - 2,
borderColorHover: base.color - 3 - 2,
borderColorFocus: base.color - 4 - 2,
borderColorPress: base.color - 5 - 2,
}
const alt1 = {
color: base.color - 1,
colorHover: base.colorHover - 1,
colorPress: base.colorPress - 1,
colorFocus: base.colorFocus - 1,
}
const alt2 = {
color: base.color - 2,
colorHover: base.colorHover - 2,
colorPress: base.colorPress - 2,
colorFocus: base.colorFocus - 2,
}
return {
base,
alt1,
alt2,
surface1,
surface2,
surface3,
inverseSurface1,
inverseActive,
surfaceActive,
}
})()
const shadows = {
light: {
shadowColor: lightShadowColorStrong,
shadowColorHover: lightShadowColorStrong,
shadowColorPress: lightShadowColor,
shadowColorFocus: lightShadowColor,
},
dark: {
shadowColor: darkShadowColorStrong,
shadowColorHover: darkShadowColorStrong,
shadowColorPress: darkShadowColor,
shadowColorFocus: darkShadowColor,
},
}
const nonInherited = {
light: {
...lightColors,
...shadows.light,
},
dark: {
...darkColors,
...shadows.dark,
},
}
const overlayThemeDefinitions = [
{
parent: 'light',
theme: {
background: 'rgba(0,0,0,0.5)',
},
},
{
parent: 'dark',
theme: {
background: 'rgba(0,0,0,0.9)',
},
},
]
const inverseSurface1 = [
{
parent: 'active',
template: 'inverseActive',
},
{
parent: '',
template: 'inverseSurface1',
},
] as any
const surface1 = [
{
parent: 'active',
template: 'surfaceActive',
},
{
parent: '',
template: 'surface1',
},
] as any
const surface2 = [
{
parent: 'active',
template: 'surfaceActive',
},
{
parent: '',
template: 'surface2',
},
] as any
const surface3 = [
{
parent: 'active',
template: 'surfaceActive',
},
{
parent: '',
template: 'surface3',
},
] as any
// --- themeBuilder ---
const themeBuilder = createThemeBuilder()
.addPalettes(palettes)
.addTemplates(templates)
.addThemes({
light: {
template: 'base',
palette: 'light',
nonInheritedValues: nonInherited.light,
},
dark: {
template: 'base',
palette: 'dark',
nonInheritedValues: nonInherited.dark,
},
})
.addChildThemes({
orange: {
palette: 'orange',
template: 'base',
},
yellow: {
palette: 'yellow',
template: 'base',
},
green: {
palette: 'green',
template: 'base',
},
blue: {
palette: 'blue',
template: 'base',
},
purple: {
palette: 'purple',
template: 'base',
},
pink: {
palette: 'pink',
template: 'base',
},
red: {
palette: 'red',
template: 'base',
},
gray: {
palette: 'gray',
template: 'base',
},
})
.addChildThemes({
alt1: {
template: 'alt1',
},
alt2: {
template: 'alt2',
},
active: {
template: 'surface3',
},
})
.addChildThemes(
{
ListItem: {
template: 'surface1',
},
SelectTrigger: surface1,
Card: surface1,
Button: surface3,
Checkbox: surface2,
Switch: surface2,
SwitchThumb: inverseSurface1,
TooltipContent: surface2,
DrawerFrame: {
template: 'surface1',
},
Progress: {
template: 'surface1',
},
RadioGroupItem: surface2,
TooltipArrow: {
template: 'surface1',
},
SliderTrackActive: {
template: 'surface3',
},
SliderTrack: {
template: 'surface1',
},
SliderThumb: inverseSurface1,
Tooltip: inverseSurface1,
ProgressIndicator: inverseSurface1,
SheetOverlay: overlayThemeDefinitions,
DialogOverlay: overlayThemeDefinitions,
ModalOverlay: overlayThemeDefinitions,
Input: surface1,
TextArea: surface1,
},
{
avoidNestingWithin: ['alt1', 'alt2'],
}
)
// --- themes ---
const themesIn = themeBuilder.build()
export type Theme = Record<keyof typeof templates.base, string> &
typeof nonInherited.light
export type ThemesOut = Record<keyof typeof themesIn, Theme>
export const themes = themesIn as ThemesOut
// --- utils ---
export function postfixObjKeys<
A extends { [key: string]: Variable<string> | string },
B extends string,
>(
obj: A,
postfix: B
): {
[Key in `${keyof A extends string ? keyof A : never}${B}`]: Variable<string> | string
} {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [`${k}${postfix}`, v])
) as any
}
// a bit odd but keeping backward compat for values >8 while fixing below
export function sizeToSpace(v: number) {
if (v === 0) return 0
if (v === 2) return 0.5
if (v === 4) return 1
if (v === 8) return 1.5
if (v <= 16) return Math.round(v * 0.333)
return Math.floor(v * 0.7 - 12)
}
export function objectFromEntries<ARR_T extends EntriesType>(
arr: ARR_T
): EntriesToObject<ARR_T> {
return Object.fromEntries(arr) as EntriesToObject<ARR_T>
}
export type EntriesType =
| [PropertyKey, unknown][]
| ReadonlyArray<readonly [PropertyKey, unknown]>
export type DeepWritable<OBJ_T> = { -readonly [P in keyof OBJ_T]: DeepWritable<OBJ_T[P]> }
export type UnionToIntersection<UNION_T> = // From https://stackoverflow.com/a/50375286
(UNION_T extends any ? (k: UNION_T) => void : never) extends (k: infer I) => void
? I
: never
export type UnionObjectFromArrayOfPairs<ARR_T extends EntriesType> =
DeepWritable<ARR_T> extends (infer R)[]
? R extends [infer key, infer val]
? { [prop in key & PropertyKey]: val }
: never
: never
export type MergeIntersectingObjects<ObjT> = { [key in keyof ObjT]: ObjT[key] }
export type EntriesToObject<ARR_T extends EntriesType> = MergeIntersectingObjects<
UnionToIntersection<UnionObjectFromArrayOfPairs<ARR_T>>
>
export function objectKeys<O extends Object>(obj: O) {
return Object.keys(obj) as Array<keyof O>
}
@ashuvssut
Copy link
Copy Markdown

ashuvssut commented Nov 10, 2025

  • From Themes docs:-
image

In the Themes docs, I saw this suggestion to follow the v3-themes code from this gist

Since we have v4 configs now, are these v3 theme related code in your gist still valid?

Sorry, I was not able to find any migration guide in the docs... so i am just generally curious, what's the difference between v3 and v4 configs?

@rome2o
Copy link
Copy Markdown

rome2o commented Mar 16, 2026

Yeah, looks like this is not up to date.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment