code/tamagui.dev/data/blog/version-one.mdx
Tamagui's optimizing compiler makes advanced optimizations using partial evaluation, CSS extraction, and ultimately tree-flattening. Where it optimizes, things get a lot faster:
<Spacer /> <Spacer /> <BenchmarkChartWeb /> <Aside>Rendering between groups of styles conditionally</Aside> <Spacer /> <Spacer />Tamagui comes in three parts:
Tamagui Core (@tamagui/core) is a style
library for React Native and web that supports 100% of the React Native API
surface in a library that only depends on React. It adds all the
goodies of modern style libraries into a cohesive package.
Tamagui Static (@tamagui/static) is an
optimizing compiler on top of Core, turning even inline styles sprinkled with
logic into atomic CSS and minimal JS, flattening your view tree for maximum
performance.
Tamagui UI (tamagui) is a component kit built on
top of the incredibly powerful theme primitives of core. That, alongside
compound-component APIs and complete size control enables a new level of control
not seen before in a UI kit.
Lets get familiar with both @tamagui/core (the styling library) and
@tamagui/static (the compiler), and how the latter optimizes the former.
The key to Tamagui's performance gains lie in view flattening or
tree-flattening: the compiler analyzes code (even across module boundaries) and
evaluates styled components away using both the AST and Node VM code evaluation.
It will fully flatten custom styled components (like <Circle />) into plain
old <div /> (or <View /> on native).
@tamagui/coreCore is a complete cross-platform style library. It allows creating typed
design systems that work with a styled factory on both React Native and web:
@tamagui/web to 31Kb @tamagui/core (gzip, excluding react).One novel feature is deterministic, easy to reason about style merging. Core
always merges styles based on the order received rather than arcane CSS
definition-order + specificity rules, no matter if those styles come through
nested styled calls, or via inline props. This resolves limitations in both
CSS and CSS-in-JS systems as they largely exist today.
Here's an example of the styled function with a few variants:
import { Stack, styled } from 'tamagui' // or '@tamagui/core'
export const Circle = styled(Stack, {
// access your tokens and theme values easily with $ props
backgroundColor: '$background',
borderRadius: '$4',
// media and pseudo styles - this would take 15+ lines of brittle JS in RN
$gtSm: {
pressStyle: {
borderRadius: '$6',
},
},
variants: {
// define variants <Circle pin="top" />
// these will flatten, even when nesting multiple styled() calls
pin: {
top: {
position: 'absolute',
top: 0,
},
},
size: {
// functional variants give incredible power and save bundle size
'...size': (size, { tokens }) => {
return {
width: tokens.size[size] ?? size,
height: tokens.size[size] ?? size,
}
},
},
} as const,
})
For more, see the configuration, styling, and variants docs.
Using createTamagui you generate a fully-typed design system with colors, tokens, spacing, sizing, fonts, themes, shorthands, animations, and more.
It works with well-optimized and easy to use hooks like useMedia and useTheme that save a ton of time over rolling these yourself.
Here's a stripped down configuration:
import { createFont, createTamagui, createTokens } from 'tamagui' // or '@tamagui/core'
const interFont = createFont({
family: 'Inter, Helvetica, Arial, sans-serif',
size: { 1: 12, 2: 14 /* ... */ },
// ... lineHeight, weight, letterSpacing, transform, style, color, face
})
const size = { 0: 0, 1: 5, 2: 10 /* ... */ }
const tokens = createTokens({
size,
space: { ...size, '-1': -5, '-2': -10 },
radius: { 0: 0, 1: 3 },
zIndex: { 0: 0, 1: 100, 2: 200 },
color: { white: '#fff', black: '#000' },
})
export default createTamagui({
fonts: {
heading: interFont,
body: interFont,
},
tokens,
themes: {
light: { bg: '#f2f2f2', color: tokens.color.black },
dark: { bg: '#111', color: tokens.color.white },
},
media: {
sm: { maxWidth: 860 },
gtSm: { minWidth: 860 + 1 },
},
shorthands: {
px: 'paddingHorizontal',
f: 'flex',
} as const,
})
Finally, the Stack and Text components work with
styled() and provide lot of nice features like hover, press, and focus styles,
typed inline style props with theme and token values, shorthands, media queries,
animations, and more.
@tamagui/coreOver a thousand bugfixes since beta with large performance and memory improvements, much higher test coverage, and compatibility with the latest and greatest in React and React Native.
Across all of Tamagui, we've bumped support to the latest versions. Due to improvements in react-native-web 18, we were also able to dramatically simplify setup, no longer having to patch the library to support classnames.
Tamagui supports React Native 0.70, React 18, React Native Web 18. It's the only Native UI library that fully supports concurrent mode on the web as well.
Early in the beta for web-only use cases, Core had the downside of requiring you
to bundle all of react-native-web in your app, not to mention the always
tricky problem of configuring your bundler to alias react-native to
react-native-web.
For 1.0, we've made the difficult steps to get @tamagui/core to have no
dependency at all on react-native (and therefore no need to setup anything in
your bundler), while still maintaining 100% compatibility with the React Native
APIs.
This unlocks a few things:
react-native-web entirely for web-only
use cases.You can get all the benefits of unified APIs across native and web plus an optimizing compiler and full-featured design system, yet actually save bundle size vs React Native Web.
De-coupling core from react-native is a big win for web bundle size, allowing
you to leave out 35Kb of size if just using core. We've shaved nearly ~12Kb
beyond that during the betas by iteratively improving code and reducing
dependencies. Today core is ~20Kb, gzipped, but can get down to ~10Kb fairly
straightforwardly.
Beyond that, we have landed refactors now that allow releasing a web-specific core which will drop a few Native-specific APIs and bring the total runtime size cost down to below 8Kb.
We've greatly optimized render performance during the beta across the board. Some of the more interesting improvements are:
useThemeThe useTheme hook is used by every Tamagui component, and is smart about
re-renders by tracking which keys are accessed by your styles to only re-render
when their values change. But it wasn't as smart about memory usage and avoiding
work up front, and was also re-parenting the React tree too often.
We saw 8% improvements in Lighthouse scores on the website homepage just by optimizing this.
useMediaSupporting SSR often means you need to hydrate using an initial value, and then re-render after hydration with the "real" media query value. We've moved over to use React's useSyncExternalStore hook, which greatly reduced code and helps avoid re-renders and hooks used per-component. It also clears us to remove the double-render that SSR based apps typically do on hydration.
Previously, Core used react-native-web to handle the final steps of taking
styles and converting them into CSS. In profiling and reading over the code, we
found this to be a large bottleneck to performance as it would iterate over
objects many times, and generate many intermediate objects in the process.
@tamagui/core is now entirely dependency-free and generates it's own styles.
In total, de-coupling saved us 4-5 loops over the generated style objects
per-component.
styled upgradesThe styled factory has undergone a number of improvements. It now supports
wrapping any component you give it, so long as that component accepts a style
prop.
styled typesWe now recommend using as const after your variants object definition to fix
some tricky issues related to some
outstanding Typescript limitations around inferring const generics.
Supporting fonts per-language is now possible with <FontLanguage />, like so. First, you can specify language-specific fonts with a suffix in your configuration, much like a sub-theme:
const bodyFontEn = createFont({
family: '"Helvetica"',
// ... per-font design tokens
})
const bodyFontMandarin = createFont({
family: '"Helvetica Mandarin"',
// ... per-font design tokens
})
export const config = createTamagui({
fonts: {
body: bodyFontEn,
body_mandarin: bodyFontMandarin,
},
})
Then you can change the family to Mandarin at any point in your React tree, with types working as expected:
import { FontLanguage, Text } from 'tamagui' // or '@tamagui/core'
export default () => (
<FontLanguage body="mandarin">
<Text fontFamily="$body"></Text>
</FontLanguage>
)
config.fonts.[fontname].faceReact Native makes loading fonts a bit trickier than the web, and the easiest
way to do it involved naming your font family differently per-weight. On the web
you'd have Helvetica and just change font-weight, but on Native you set the
family from Helvetica to Helvetica Bold instead of the weight.
Tamagui added support for this through
the face option on font configurations.
themeShallow propThe theme prop in Tamagui by default will re-theme all sub-component. Tamagui
now supports the boolean themeShallow prop on any Tamagui component, which
will only theme that exact component, leaving all children components with the
same theme as their parent.
loadTheme and updateTheme helpersThemes load in a large variety of tokens for spacing, sizing, radius, colors, and more. They are incredibly powerful, but they also have some cost in bundle size.
The loadTheme utility function means you can only load the default themes you
want to serve, saving on bundle size, and then add in alternative themes later
on.
Meanwhile updateTheme gives you the ability to dynamically modify themes on
the fly, changing any of their values. On the web, themes work through CSS
variables, meaning updateTheme is incredibly fast as it avoids all React
re-rendering if no theme values are currently being relied on for dynamic
styles.
useMediaPropsActive hookThis hook is useful for authoring your own custom components built on Tamagui. The Tamagui UI kit makes use of this hook extensively. Tamagui has a philosophy that everything Just Works™️ at runtime as well as compile-time. The compile-time side just makes it all a lot faster. By working fully at runtime, it means you have more flexiblity (no need to use any plugins at all), but also lots of power. For example, if Tamagui didn't work fully runtime, you couldn't support animations with your design system at all.
The useMediaPropsActive is an important part of that functionality, making it
easy to properly access the "currently active" set of styles given the current
screen size. Here's an example:
import { Stack, StackProps, useMediaPropsActive } from 'tamagui' // or '@tamagui/core'
const CustomWidget = (props: StackProps) => {
const activeProps = useMediaPropsActive(props)
console.log(`The current color for this screen size is`, activeProps.backgroundColor)
return <Stack {...modifyProps(activeProps)} />
}
export default () => (
<CustomWidget
backgroundColor="red"
$large={{
backgroundColor: 'green',
}}
/>
)
@tamagui/staticStatic is a babel and node-based optimizing compiler, specifically for frontend React code. It supports web and native through a plugin interface that has adapters for Webpack, Vite, Next, React Native, and Expo. Over a dozen major improvements to the compiler have made their way in since beta. Some of the highlights are:
On the web, render performance is much improved, especially for interactive (pseudo) and responsive (media query) styles. It improves Lighthouse scores 10-25%.
<Image title="The compiler improves the Lighthouse score of the homepage of https://tamagui.dev from 85 to 96" src="/banner-lighthouse.jpg" size="hero" height={325} width={761} resizeMode="contain" mt="$2" mb="$4" />
For more information on how it works and why it's important, read our introduction to the compiler article.
@tamagui/staticThe compiler uses babel to parse and optimize components, which it can get
away with for performance largely because it only needs to parse a subset of
your files, and it only needs to look for a few areas - namely JSX elements and
styled() functions.
During the beta we landed many performance improvements bringing average parse time down to below 6ms for a longer file on a modern laptop. By default now Tamagui avoids parsing many more files through quick heuristics. It's now also fully thread-safe, uses more caching, and has been fine-tuned for memory usage.
Previously, Tamagui Static only knew how to optimize components found in your
separated design system package. But it's both common and desirable to have
one-off styled() definitions that are just used for small areas of your app
that live alongside those areas, outside your design system.
The compiler now supports analyzing components outside of just your design
system, allowing for even less friction when writing apps. It means you can put
your styled() definitions anywhere without worrying, and Tamagui Static will
load and optimize those components as it discovers them.
Tamagui now fully works with Vite 3 and 4, both with the
compiler and without. A simple vite.config.js would look like this:
import { tamaguiPlugin } from '@tamagui/vite-plugin'
import react from '@vitejs/plugin-react-swc'
import { defineConfig } from 'vite'
export default defineConfig({
clearScreen: false,
plugins: [
react(),
tamaguiPlugin({
components: ['tamagui'],
config: 'tamagui.config.ts',
}),
],
})
Most of the work during beta focused on correctness. We've increased testing to
cover many more areas, and landed a number of important correctness fixes
alongside the ones in core.
Tamagui UI is a suite of React components and hooks that adapt nicely to both Native and Web.
We've seen the addition of seven new components. Each comes with compound component API for much more complete control over each piece of the components visuals and behavior.
We owe thanks to both Radix and Floating UI for making these much easier to build.
Like the existing components in Tamagui, every new component is sizable, themeable, and able to be automatically styled using your design system.
The new components are:
This was an excellent test of the unified animation drivers on Tamagui, and unified interaction primitives of React Native, and we're happy with how it's turned out.
Internally, we've come up with some helpers for each animation driver to support a consistent API for taking a number value and interpolating it with some style properties. Each driver handles this quite differently on the surface, but the Tamagui drivers now give a consistent interface across them all.
Show content in a Dialog that can adapt to another component depending on the screen size.
<HeroContainer tinted> <DialogDemo /> </HeroContainer>Show a Dialog specialized for confirming or denying an action with AlertDialog.
<HeroContainer tinted> <AlertDialogDemo /> </HeroContainer>AlertDialog is the first of our components to include the native prop. When
set to true, it adapts the element to use platform native visual containers when
possible. For AlertDialog, this means that on iOS and Android, you'll see a
native Alert dialog prompt.
A draggable Slider allows users to input values within a range.
<HeroContainer tinted> <SliderDemo /> </HeroContainer>Label has been updated to work with all the new form inputs.
<HeroContainer tinted> <LabelDemo /> </HeroContainer>Display content with a header, footer, background image, title, subtitle and description using Card.
<HeroContainer tinted> <CardDemo /> </HeroContainer>ListItem allows you to display content with a title, subtitle, before and after images or icons, and more.
<HeroContainer tinted> <ListItemDemo /> </HeroContainer>The Group component has been split into
XGroup and YGroup to match Stacks. It's been upgraded
across the board with the ability to be scrollable and to better handle passing
children styles.
Tamagui now exports ScrollView, the exact same as React Native ScrollView but with all Tamagui props supported.
The monorepo now includes code/kitchen-sink, a native app that demos every
component that comes in Tamagui. This has helped us quickly iterate and improve
components and their correctness across Android and iOS. Android especially
received a wide variety of fixes during the betas.
create-tamagui underwent some big improvements that will set it up to be much
more useful going forward. We moved starters into the monorepo, which was tricky
but lets us iterate much more rapidly on them and test them e2e for each
release. We're using a custom ~/.tamagui home directory and git clone that
gives us a much smoother upgrade experience, especially in future versions. The
starer-free starter repo has had extensive polish as well, simplifying the
setup, removing the need for watch commands, and properly building to production
across all features for web, iOS and Android.
We added a new animation driver, @tamagui/animations-react-native that works
with the built-in Animation API of React Native. This is in addition to the
existing @tamagui/animations-css and @tamagui/animations-reanimated drivers.
The advantages of this driver are that you pay no extra cost for spring
animations on the web when you're already using react-native-web, and a
simpler setup compared to Reanimated.
During the beta we landed a wide variety of correctness fixes, tests and runtime
safeguards for SSR and landed early support for React Server Components. We've
tested them in early versions of Hydrogen and Next.js and they should generally
work when the environment variable ENABLE_RSC is set.
We've landed Vite support. Check out the Vite guide for how to install Tamagui with Vite.
As a temporary measure, we released a modernized version of react-native-web.
It improves tree shaking, lands complete concurrent support for React 18, and
strips out a few deprecated areas.
We've updated Tamagui's base React version to be 18, and landed a large amount
of fixes relating to concurrent mode. Every feature in Tamagui now fully
supports concurrent mode, and with react-native-web-lite, so does every
feature in the React Native API surface for web.
@tamagui/next-themeSee the Next.js guide for more on how to use this helpful library that automatically handles light/dark/system color scheme preferences.
@tamagui/theme-base upgradesThe default theme package now includes _active sub-themes, paving the way for
a consistent way to style all active states across every component in tamagui.
It also now includes theme values for color1 => color12 as part of each
theme. This gives you granular access outside of the more specific color values
like background, or borderColor.
Tamagui now has several apps in production in both app stores. One larger one built by a household name company is currently under NDA, but we can announce soon. We're proud of how active the Discord has become with users building high quality apps!
More than any other section in this release, the biggest amount of effort for 1.0 went into correctness. Tamagui has steadily landed fixes across every feature and component since Beta, and the difference between early this year and now is astounding, with a ton of edge cases handled automatically now.
We've been building a great community, especially on Discord. Check out the Community page for more information including diverse community-maintained starter kits and Figma files.
The docs have undergone continuous improvements. The compiler now has an extensive article breaking down the whys and hows of how it works. And all the guides have been iterated on and tested from scratch to verify they work.
We've expanded our testing signficantly: 10x more tests, package.json linting, full CI/CD, stricter linting in general, and a few big integration tests.
Tamagui is stable and free of all blocking bugs across every feature, but there's a so much more to go to make every part of it simpler, lighter, faster, and more correct both in types and behavior.
Writing about what comes after 1.0 is a blog post to itself. For now, next.md is a living document of upcoming features and fixes.
The next few minor releases will focus on improving types, accessibility, animations, test coverage, and solidifying the newer components. We'll be setting up stricter release processes, and improving contributor experience.
There are many exciting features on the table as well, including:
a:hover p.tamagui build - build to plain css and JS for compatibility with any
bundler.native prop for platform-specific output on Select, Dialog,
Popover, Sheet and more.<Theme /> custom tokens.<Debug /> to show nice visualizations of what's happening inside an entire
tree of components.<Skeleton /> to automatically render skeleton variants for every component
in a tree.<Menu />, <Radio /> or <Toggle /> equivalent, <Tabs />,
<Accordion />, <Autocomplete /> and more.focusWithinStyle.height="$lg".<IntroParagraph>Tamagui is fully funded by GitHub Sponsors.</IntroParagraph>
I love working on Tamagui, and there's a wealth of concrete and interesting ways for it to mature. The motivation is to take things slowly, do them right, and stay fully OSS.
<IntroParagraph> In order to make this work, I'll need sponsors, and I want to make that worth it. I'm setting up a range of pretty cool **Sponsor Benefits** going forward: </IntroParagraph>Docs can run alongside Studio in the future as part of a suite of tools for your frontend. Studio will have realtime collaboration and allows you to edit your apps themes live with HMR. Tweak colors, themes, tokens, styles, icons, fonts, and more across every pseudo state on every component.
Studio hooks into your app at runtime to achieve this thanks to the big work we've aleady done pre-analyzing your components and their types and props.
It will also export and import between JSON and Figma.
Please do consider sponsoring!
<Spacer /> <SponsorButton />We've had 38 new contributors since early this year, exponentially increasing! Thanks to all of them, and also to the many QA testers that join in our Discord chat every day :).
Early sponsors have made a huge difference: I owe a large thank you to Vercel, Codingscape, QuestPortal and BeatGig, the first corporate sponsors ❤️.
Tamagui draws inspiration and small areas of code from some incredible other libraries: Radix provided much of the compound component APIs, Floating UI handles all of the floating elements positioning, Moti which served as a start point for the animation drivers, and Framer Motion for the nifty AnimatePresence functionality.