mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-30 06:05:14 +00:00
The transplant rebuilt the tree on upstream tamagui and carried our own packages forward by hand, which silently lost seven @hanzo_network/* packages and apps/team — 735 files with no other home in the workspace. They are back at the tip of the old lineage's content. pkgs/ai is NOT restored: @hanzo/ai is published from hanzo-js/ai, so a copy here would be the duplicate we are trying to avoid. templates/expo-router is NOT restored: apps/starters/expo-router already declares that exact package name upstream. Also fixes a rebrand defect: the blanket tamagui->hanzogui rule rewrote upstream's GitHub path into github.com/hanzogui/hanzogui, a repo that has never existed, and it shipped in repository.url for every published package. The transform now maps the remote to hanzoai/gui and repairs a tree already branded wrong, so a future sync cannot reintroduce it. Co-authored-by: Hanzo Dev <[email protected]>
771 lines
21 KiB
Plaintext
771 lines
21 KiB
Plaintext
---
|
||
title: How to Build a Button
|
||
description: Learn how to create a powerful yet simple API for a button with Gui and
|
||
compound components
|
||
hero: HowToBuildAButton
|
||
demoName: BuildAButton
|
||
---
|
||
|
||
<HeroContainer noScroll>
|
||
<BuildAButtonDemo />
|
||
</HeroContainer>
|
||
|
||
<IntroParagraph>
|
||
Deceptively innocuous, designing a robust yet flexible API even for a Button can be
|
||
filled with surprising challenges.
|
||
</IntroParagraph>
|
||
|
||
More than just text in a box, buttons need icons, themes, variants, sizes, often
|
||
loading indicators and several interactive states, and always a delicate balance
|
||
between sub-components' positions, spacing and style.
|
||
|
||
All of this ideally delivered in a simple API that still allows complete
|
||
customization.
|
||
|
||
Gui allows for building a button with "compound components", a concept
|
||
popularized by [Radix](https://www.radix-ui.com/). With the introduction of
|
||
`createStyledContext` in version 1.28 this pattern has become especially easy,
|
||
so we wrote this guide to show off building more advanced compound component
|
||
APIs. While this guide covers a button, this is applicable to many types of UI
|
||
components.
|
||
|
||
So what are compound components? Simply, it's when you have, say, a
|
||
`<Button />` that wants children `<Button.Text />` and `<Button.Icon />`.
|
||
|
||
And why do they exist? Well, we'll explain that and more in this guide.
|
||
|
||
Keep in mind that this is an advanced use case. Usually you can just grab
|
||
Gui components off the shelf and use them with inline styles. But sometimes
|
||
you need components that want multiple related children.
|
||
|
||
<Notice>
|
||
This guide is more of a primer on designing compound components, teaching advanced
|
||
concepts that aren't necessary if you just want to build your app. If you just want a
|
||
simple, ready to use Button, you can use the [Gui Button](/docs/components/button)
|
||
itself directly.
|
||
</Notice>
|
||
|
||
### The Final Result
|
||
|
||
To the point, here is the final code we'll end up with for our Button, ready to
|
||
copy and paste right into your app:
|
||
|
||
```tsx
|
||
import { getSize, getSpace } from '@hanzogui/get-token'
|
||
import {
|
||
GetProps,
|
||
SizeTokens,
|
||
View,
|
||
Text,
|
||
createStyledContext,
|
||
styled,
|
||
useTheme,
|
||
withStaticProperties,
|
||
} from '@hanzogui/web'
|
||
import { cloneElement, isValidElement, useContext } from 'react'
|
||
|
||
export const ButtonContext = createStyledContext({
|
||
size: '$md' as SizeTokens,
|
||
})
|
||
|
||
export const ButtonFrame = styled(View, {
|
||
name: 'Button',
|
||
context: ButtonContext,
|
||
backgroundColor: '$background',
|
||
alignItems: 'center',
|
||
flexDirection: 'row',
|
||
|
||
hoverStyle: {
|
||
backgroundColor: '$backgroundHover',
|
||
},
|
||
|
||
pressStyle: {
|
||
backgroundColor: '$backgroundPress',
|
||
},
|
||
|
||
variants: {
|
||
size: {
|
||
'...size': (name, { tokens }) => {
|
||
return {
|
||
height: tokens.size[name],
|
||
borderRadius: tokens.radius[name],
|
||
// note the getSpace and getSize helpers will let you shift down/up token sizes
|
||
// whereas with gap we just multiply by 0.2
|
||
// this is a stylistic choice, and depends on your design system values
|
||
gap: tokens.space[name].val * 0.2,
|
||
paddingHorizontal: getSpace(name, {
|
||
shift: -1,
|
||
}),
|
||
}
|
||
},
|
||
},
|
||
} as const,
|
||
|
||
defaultVariants: {
|
||
size: '$md',
|
||
},
|
||
})
|
||
|
||
type ButtonProps = GetProps<typeof ButtonFrame>
|
||
|
||
export const ButtonText = styled(Text, {
|
||
name: 'ButtonText',
|
||
context: ButtonContext,
|
||
color: '$color',
|
||
userSelect: 'none',
|
||
|
||
variants: {
|
||
size: {
|
||
'...fontSize': (name, { font }) => ({
|
||
fontSize: font?.size[name],
|
||
}),
|
||
},
|
||
} as const,
|
||
})
|
||
|
||
const ButtonIcon = (props: { children: any }) => {
|
||
const { size } = useContext(ButtonContext.context)
|
||
const smaller = getSize(size, {
|
||
shift: -2,
|
||
})
|
||
const theme = useTheme()
|
||
return isValidElement(props.children)
|
||
? cloneElement(props.children, {
|
||
size: smaller.val * 0.5,
|
||
color: theme.color.get(),
|
||
})
|
||
: null
|
||
}
|
||
|
||
export const Button = withStaticProperties(ButtonFrame, {
|
||
Props: ButtonContext.Provider,
|
||
Text: ButtonText,
|
||
Icon: ButtonIcon,
|
||
})
|
||
```
|
||
|
||
Now you can use your button like so:
|
||
|
||
```tsx
|
||
export default () => (
|
||
<Button>
|
||
<Button.Icon>
|
||
<Moon />
|
||
</Button.Icon>
|
||
<Button.Text>hi</Button.Text>
|
||
</Button>
|
||
)
|
||
|
||
// multiple icons:
|
||
export default () => (
|
||
<Button>
|
||
<Button.Icon>
|
||
<Moon />
|
||
</Button.Icon>
|
||
<Button.Text>hi</Button.Text>
|
||
<Button.Icon>
|
||
<Moon />
|
||
</Button.Icon>
|
||
</Button>
|
||
)
|
||
```
|
||
|
||
### Styling Text Color
|
||
|
||
In v2, `color` is not a direct prop on the Button frame. Instead, style your
|
||
text through `Button.Text`. For interactive color changes on hover or press, use
|
||
Gui's `group` prop to coordinate styles between parent and child:
|
||
|
||
```tsx
|
||
export default () => (
|
||
<Button group="btn" bg="pink">
|
||
<Button.Text color="blue" $group-btn-hover={{ color: 'red' }}>
|
||
Hello world
|
||
</Button.Text>
|
||
</Button>
|
||
)
|
||
```
|
||
|
||
By setting `group="btn"` on the Button frame, any child can reference that group
|
||
for interactive styles using `$group-btn-hover`, `$group-btn-press`, and so on.
|
||
This is more flexible and explicit than passing `color` directly, as it lets you
|
||
independently control text, icon, and frame styles for every interaction state.
|
||
|
||
This may only be about a hundred lines of code, but there's a lot to take in. And
|
||
behind the scenes, there's a lot going on to make this possible.
|
||
|
||
### From the Start
|
||
|
||
This guide will build up to this complete example from the ground up, which
|
||
should help explain both why many patterns exist, and also how they work.
|
||
|
||
We start with the assumption that we have a design system set up with tokens
|
||
that use keys `sm`, `md`, and `lg` and a few themes:
|
||
|
||
```tsx
|
||
import { createGui, createTokens } from '@hanzogui/core'
|
||
|
||
export default createGui({
|
||
tokens: createTokens({
|
||
size: {
|
||
sm: 38,
|
||
md: 46,
|
||
lg: 60,
|
||
},
|
||
space: {
|
||
sm: 15,
|
||
md: 20,
|
||
lg: 25,
|
||
},
|
||
radius: {
|
||
sm: 4,
|
||
md: 8,
|
||
lg: 12,
|
||
},
|
||
// ... the rest of your tokens
|
||
}),
|
||
|
||
themes: {
|
||
light: {
|
||
background: '#fff',
|
||
color: '#000',
|
||
},
|
||
|
||
// define a Button sub-theme, see the Themes docs for more
|
||
light_Button: {
|
||
background: '#ccc',
|
||
backgroundPress: '#bbb', // darker background on press
|
||
backgroundHover: '#ddd', // lighter background on hover
|
||
color: '#222',
|
||
},
|
||
},
|
||
|
||
// ... the rest of your hanzogui.config.ts
|
||
})
|
||
```
|
||
|
||
If you'd like to see a more complete work-up of a Gui config, check out
|
||
[the Simple Web starter repo source code](https://github.com/hanzoai/gui/blob/main/apps/starters/simple-web/src/hanzogui.config.ts)
|
||
or go ahead and create that starter using `npm create hanzogui@latest`.
|
||
|
||
With that in mind, we can create the outer frame of the button fairly simply,
|
||
like so:
|
||
|
||
```tsx
|
||
import { View, styled } from '@hanzogui/core'
|
||
|
||
const ButtonFrame = styled(View, {
|
||
// the name indicates to use the sub-theme `Button`
|
||
// since we defined light_Button, if our theme is light, this component
|
||
// will always use the values from our `light_Button` theme
|
||
name: 'Button',
|
||
|
||
alignItems: 'center',
|
||
flexDirection: 'row',
|
||
|
||
// our $ prefixed values look for theme first, then fallback to tokens
|
||
backgroundColor: '$background', // #ccc
|
||
hoverStyle: {
|
||
backgroundColor: '$backgroundHover', // #ddd
|
||
},
|
||
pressStyle: {
|
||
backgroundColor: '$backgroundPress', // #bbb
|
||
},
|
||
|
||
// these all use tokens
|
||
// note that size tokens are used only for these properties:
|
||
// width, height, minWidth, minHeight, maxWidth and maxHeight
|
||
height: '$md', // 46
|
||
|
||
// meanwhile our radius token is used here
|
||
borderRadius: '$md', // 8
|
||
|
||
// and space tokens are used for all others
|
||
paddingHorizontal: '$sm', // 25
|
||
})
|
||
```
|
||
|
||
This gets us a simple rounded rectangle that uses the `md` tokens from your
|
||
design system and the `background` styles from your theme.
|
||
|
||
We set the `name` property to `"Button"`, which tells Gui to check for a
|
||
sub-theme `Button` that extends the current one. So since we have a theme
|
||
`light` and we have a sub-theme `light_Button`, Gui finds the theme
|
||
`light_Button` and apply it to this component, getting the `background` value
|
||
from that theme (assuming we have our base theme set to light).
|
||
|
||
Finally we set the `hoverStyle` and `pressStyle` so our button frame looks nice
|
||
when hovered or pressed.
|
||
|
||
Next we'll want a Text component to go inside:
|
||
|
||
```tsx
|
||
import { Text, styled } from '@hanzogui/core'
|
||
|
||
export const ButtonText = styled(Text, {
|
||
name: 'ButtonText',
|
||
color: '$color',
|
||
fontFamily: '$body',
|
||
fontSize: '$md',
|
||
lineHeight: '$md',
|
||
userSelect: 'none',
|
||
})
|
||
```
|
||
|
||
This is pretty similar to the frame, just with its own name and with a `color`
|
||
set rather than a `background`. Since we didn't define a `light_ButtonText`
|
||
theme, this will fall back to the color defined on the light theme. Finally,
|
||
button text usually isn't selectable so we set `userSelect` to none.
|
||
|
||
We can use our simple button. It's nicely themed, but only renders at one size:
|
||
|
||
```tsx
|
||
export default () => (
|
||
<Button>
|
||
<ButtonText>Hello world</ButtonText>
|
||
</Button>
|
||
)
|
||
```
|
||
|
||
So we'll make it sizeable. First, let's look at how we'd solve this without the
|
||
new `createStyledContext` helper, so we can understand why it exists.
|
||
|
||
We can add some variants:
|
||
|
||
```tsx
|
||
const ButtonFrame = styled(View, {
|
||
name: 'Button',
|
||
backgroundColor: '$background',
|
||
alignItems: 'center',
|
||
flexDirection: 'row',
|
||
|
||
variants: {
|
||
size: {
|
||
sm: {
|
||
height: '$sm',
|
||
borderRadius: '$sm',
|
||
gap: 4,
|
||
},
|
||
md: {
|
||
height: '$md',
|
||
borderRadius: '$md',
|
||
gap: 6,
|
||
},
|
||
lg: {
|
||
height: '$lg',
|
||
borderRadius: '$lg',
|
||
gap: 8,
|
||
},
|
||
},
|
||
} as const,
|
||
})
|
||
```
|
||
|
||
Still, this is somewhat repetitive and prone to typos. To avoid duplication while
|
||
also working even if we add new sizes to our design system in the future, we can
|
||
use the handy Gui [Spread Variants](/docs/core/variants#spread-variants):
|
||
|
||
```tsx
|
||
import { View, styled } from '@hanzogui/core'
|
||
|
||
const ButtonFrame = styled(View, {
|
||
name: 'Button',
|
||
backgroundColor: '$background',
|
||
alignItems: 'center',
|
||
flexDirection: 'row',
|
||
|
||
variants: {
|
||
size: {
|
||
'...size': (name, { tokens }) => {
|
||
return {
|
||
height: tokens.size[name],
|
||
borderRadius: tokens.radius[name],
|
||
gap: tokens.space[name].val * 0.2,
|
||
}
|
||
},
|
||
},
|
||
} as const,
|
||
})
|
||
|
||
export const ButtonText = styled(Text, {
|
||
name: 'ButtonText',
|
||
color: '$color',
|
||
userSelect: 'none',
|
||
|
||
variants: {
|
||
size: {
|
||
'...fontSize': (name, { font }) => ({
|
||
fontSize: font?.size[name],
|
||
}),
|
||
},
|
||
} as const,
|
||
})
|
||
```
|
||
|
||
<Notice>
|
||
The `...size` spread variant `name` defaults to implicit any. Update `tsconfig.json`'s
|
||
`"noImplicitAny": false` to avoid TypeScript errors.
|
||
</Notice>
|
||
|
||
So now we can pass in our new `size` property:
|
||
|
||
```tsx
|
||
<ButtonFrame size="$md">
|
||
<ButtonText size="$md" />
|
||
</ButtonFrame>
|
||
```
|
||
|
||
Let's clean up these two components so they are more clearly meant to be
|
||
used together (and are easier to import for users):
|
||
|
||
```tsx
|
||
export const Button = ButtonFrame as typeof ButtonFrame & {
|
||
Text: typeof ButtonText
|
||
}
|
||
|
||
Button.Text = Text
|
||
```
|
||
|
||
What is this whole `typeof ButtonFrame` thing? It's just TypeScript being
|
||
awkward. Since Gui uses this pattern internally for many components, we've
|
||
made a small helper:
|
||
|
||
```tsx
|
||
import { withStaticProperties } from '@hanzogui/core'
|
||
|
||
export const Button = withStaticProperties(ButtonFrame, {
|
||
Text: ButtonText,
|
||
})
|
||
```
|
||
|
||
Which is functionally the same as the above. So, now our users can do:
|
||
|
||
```tsx
|
||
import { Button } from './OurButton'
|
||
|
||
export default () => (
|
||
<Button size="$md">
|
||
<Button.Text size="$md">Hello world</Button.Text>
|
||
</Button>
|
||
)
|
||
```
|
||
|
||
### Improving the API
|
||
|
||
Great. We've gotten our custom Button exported. But there's something quite
|
||
unfortunate about our API as it stands, and that is that we have to always pass
|
||
`size` to both components. This is brittle and ugly.
|
||
|
||
One typical way to solve this would be to abstract both of these components into
|
||
our own React component:
|
||
|
||
```tsx
|
||
const Button = ({
|
||
size,
|
||
children,
|
||
textProps,
|
||
...props
|
||
}: ViewProps & {
|
||
textProps?: TextProps
|
||
}) => (
|
||
<ButtonFrame size={size} {...props}>
|
||
<ButtonText size={size} {...textProps}>
|
||
{children}
|
||
</ButtonText>
|
||
</ButtonFrame>
|
||
)
|
||
```
|
||
|
||
But suddenly we have issues. What if a user wants to show an icon? We need to
|
||
add `icon` and `iconProps`. And what if they want to change the order? You may
|
||
reach for something like `direction="reverse"`. But then what if they want a
|
||
loading indicator after, but an icon before. Or maybe they want to show big text
|
||
above, and smaller text below.
|
||
|
||
The problem with abstracting your Button into a single component is that the
|
||
internals are surprisingly flexible, and ideally they want to each be styled
|
||
independently.
|
||
|
||
Abstracting the tree structure that's output is our problem. The compound
|
||
component API gives us full control - we can customize every piece, even using
|
||
the existing `styled` function to re-style each piece, before re-exporting it
|
||
again:
|
||
|
||
```tsx
|
||
import { withStaticProperties } from '@hanzogui/core'
|
||
|
||
import { Button } from './OurButton'
|
||
|
||
const CustomButtonFrame = styled(Button, {
|
||
// override some styles
|
||
})
|
||
|
||
const CustomButtonText = styled(Button.Text, {
|
||
// override some styles
|
||
})
|
||
|
||
export const CustomButton = withStaticProperties(CustomButtonFrame, {
|
||
Text: CustomButtonText,
|
||
})
|
||
|
||
export default () => (
|
||
<CustomButton>
|
||
<CustomButton.Text>Hello world</CustomButton.Text>
|
||
<CustomButton.Text size="$sm">(Smaller text)</CustomButton.Text>
|
||
</CustomButton>
|
||
)
|
||
```
|
||
|
||
Not to mention by staying with the `styled` world, the Gui optimizing
|
||
compiler extracts CSS and optimizes our Button components. Note that components
|
||
using `createStyledContext` won't be flattened, but CSS extraction and style
|
||
optimization still apply - see the [createStyledContext docs](/docs/core/create-styled-context)
|
||
for details.
|
||
|
||
So we want our compound component API both for ergonomics and for performance,
|
||
but we need some way to thread our `size` property down from the parent frame to
|
||
the inner sub-components.
|
||
|
||
### Solving size
|
||
|
||
In React this is typically what context is used for.
|
||
|
||
Here's how we'd solve for size using context in plain-old React, while still
|
||
keeping the compound API:
|
||
|
||
```tsx
|
||
import { createContext } from 'react'
|
||
import { SizeTokens, GetProps, withStaticProperties } from '@hanzogui/core'
|
||
import { Button } from './OurButton'
|
||
|
||
const SizeContext = createContext<SizeTokens>('$md')
|
||
|
||
const ButtonFrame = Button.styleable(
|
||
({ size = '$md', ...props }: GetProps<typeof OGB.ButtonFrame>) => {
|
||
return (
|
||
<SizeContext.Provider value={size}>
|
||
<Button size={size} {...props} />
|
||
</SizeContext.Provider>
|
||
)
|
||
}
|
||
)
|
||
|
||
const ButtonText = Button.Text.styleable((props: GetProps<typeof OGB.ButtonText>) => {
|
||
const size = useContext(SizeContext)
|
||
return <Button.Text size={size} {...props} />
|
||
})
|
||
|
||
export const NewButton = withStaticProperties(ButtonFrame, {
|
||
Text: ButtonText,
|
||
})
|
||
```
|
||
|
||
What the hell is `ButtonText.styleable` and `ButtonFrame.styleable`?
|
||
|
||
While it's explained in [the styled docs](/docs/core/styled#styleable),
|
||
the short of it is: if you want a functional component that returns a styled
|
||
component _to then be able to be styled again_, you need `styleable`.
|
||
|
||
That's because merging things like themes, animations, variants, media queries,
|
||
and pseudo queries is quite complex, and if Gui doesn't know that there is a
|
||
functional component in between some styled components it can lead to unexpected
|
||
merging of styles.
|
||
|
||
More importantly, the above code is once again verbose, duplicative, and
|
||
brittle. For example sharing more than just `size` across these components would
|
||
require a pretty significant refactor of the context with a lot of logic
|
||
memoizing the context value properly. And, once again, by moving into functional
|
||
components, your base level views now won't be as optimized by the compiler.
|
||
|
||
### The solution, createStyledContext
|
||
|
||
Finally we get to use `createStyledContext`. We just change from `createContext`
|
||
to:
|
||
|
||
```tsx
|
||
import { createStyledContext } from '@hanzogui/core'
|
||
|
||
export const ButtonContext = createStyledContext({
|
||
size: '$md' as SizeTokens,
|
||
})
|
||
```
|
||
|
||
This returns a slightly modified `React.Context` just like
|
||
`createContext` does, so you can use it with `useContext` later on. We
|
||
still export a `.context` object with the original type just in case there's any
|
||
issue.
|
||
|
||
And now, bringing back the full example, we add an extra property to each
|
||
`styled` component, `context`:
|
||
|
||
```tsx
|
||
import { View, styled, createStyledContext } from '@hanzogui/core'
|
||
|
||
export const ButtonContext = createStyledContext({
|
||
size: '$md' as SizeTokens,
|
||
})
|
||
|
||
const ButtonFrame = styled(View, {
|
||
name: 'Button',
|
||
context: ButtonContext,
|
||
backgroundColor: '$background',
|
||
alignItems: 'center',
|
||
flexDirection: 'row',
|
||
|
||
variants: {
|
||
size: {
|
||
'...size': (name, { tokens }) => {
|
||
return {
|
||
height: tokens.size[name],
|
||
borderRadius: tokens.radius[name],
|
||
gap: tokens.space[name].val * 0.2,
|
||
}
|
||
},
|
||
},
|
||
} as const,
|
||
})
|
||
|
||
const ButtonText = styled(Text, {
|
||
name: 'ButtonText',
|
||
context: ButtonContext,
|
||
color: '$color',
|
||
userSelect: 'none',
|
||
|
||
variants: {
|
||
size: {
|
||
'...fontSize': (name, { font }) => ({
|
||
fontSize: font?.size[name],
|
||
}),
|
||
},
|
||
} as const,
|
||
})
|
||
|
||
export const Button = withStaticProperties(ButtonFrame, {
|
||
Text: ButtonText,
|
||
Props: ButtonContext.Provider,
|
||
})
|
||
```
|
||
|
||
...and we've finally arrived at our destination! We can now do the following:
|
||
|
||
```tsx
|
||
import { Button } from './OurButton'
|
||
|
||
export default () => (
|
||
<Button size="$md">
|
||
<Button.Text>Hello world</Button.Text>
|
||
</Button>
|
||
)
|
||
```
|
||
|
||
This time since `Button` knows the `size` property is defined in the `context`
|
||
it will automatically pass size down from Button to Text, just like our
|
||
hand-rolled version did.
|
||
|
||
But - we don't have to write terribly verbose and brittle code, we get nice
|
||
types and memoization automatically, and the optimizing compiler is happy.
|
||
|
||
Notice we also exported a new property on Button, `Button.Props`.
|
||
|
||
Since `createStyledContext` returns a regular `React.Context` value, this works
|
||
the same as any other React provider. We can now control the variant from
|
||
anywhere above in the React tree:
|
||
|
||
```tsx
|
||
import { Button } from './OurButton'
|
||
|
||
export default () => (
|
||
<Button.Props size="$md">
|
||
<Button>
|
||
<Button.Text>Hello world</Button.Text>
|
||
</Button>
|
||
</Button.Props>
|
||
)
|
||
```
|
||
|
||
The only difference from `createContext` is that the `createStyledContext`
|
||
Provider just takes props directly rather than inside a `value` object (and of
|
||
course the memoization comes for free).
|
||
|
||
### Adding an Icon
|
||
|
||
Are we done?
|
||
|
||
Not quite. One final common piece of a Button is having an icon that sizes and
|
||
colors properly as well. But an icon usually comes from a third-party
|
||
library, an SVG, or perhaps an icon font.
|
||
|
||
Let's add a new component, `Button.Icon` and make this work.
|
||
|
||
Instead of going through `styled`, this component will be just a plain
|
||
functional component. It will still work nicely with the Gui themes and
|
||
sizes, showing how you can use them together with external React components.
|
||
|
||
```tsx
|
||
import { getTokens, useTheme } from '@hanzogui/core'
|
||
import * as React from 'react'
|
||
|
||
const ButtonIcon = (props: { children: React.ReactNode }) => {
|
||
const { size } = React.useContext(ButtonContext)
|
||
const tokens = getTokens()
|
||
const smallerSize = tokens.size[size].val * 0.5
|
||
const theme = useTheme()
|
||
return React.cloneElement(props.children, {
|
||
width: smallerSize,
|
||
height: smallerSize,
|
||
color: theme.color.get(),
|
||
})
|
||
}
|
||
|
||
// add it to your Button:
|
||
|
||
export const Button = withStaticProperties(ButtonFrame, {
|
||
Text: ButtonText,
|
||
Icon: ButtonIcon,
|
||
Props: ButtonContext.Provider,
|
||
})
|
||
```
|
||
|
||
Now we can use it like this, assuming your Icon accepts `width`, `height`, and
|
||
`color`:
|
||
|
||
```tsx
|
||
import { MyIcon } from 'some-icon-library'
|
||
|
||
export default () => (
|
||
<Button size="$lg">
|
||
<Button.Icon>
|
||
<MyIcon />
|
||
</Button.Icon>
|
||
<Button.Text>Hello world</Button.Text>
|
||
</Button>
|
||
)
|
||
```
|
||
|
||
Of course you can make your `Button.Icon` work a bit differently if you'd like,
|
||
say changing out `children` + `cloneElement` for something like
|
||
`<Button.Icon icon={MyIcon} />`. It's up to you.
|
||
|
||
### Conclusion
|
||
|
||
We hope this has been helpful in explaining a variety of Gui features and
|
||
some of the benefits and ideas behind compound components.
|
||
|
||
There's certainly further you can go in building out your Button, but we think
|
||
that in under 150 lines of code you're getting a nearly ideal API, fully typed
|
||
sizing and themes, and a great balance of customization to optimization. Note
|
||
that the compiler will still extract CSS and optimize these components, though
|
||
flattening won't apply when using `createStyledContext`.
|
||
|
||
If you are working on a smaller app, say one that shows only a few buttons at a
|
||
time or with a small set of button variations, then abstracting your Button into
|
||
a single functional Button component is totally fine.
|
||
|
||
But we'd encourage you to give the compound component API a try and see
|
||
how you like it.
|
||
|
||
With the new Gui APIs, we look forward to never having to carefully add a
|
||
forwardRef, re-jig the types, and then manually thread context just to share
|
||
some props between parent and child components, especially when even after all
|
||
of that we'd still somehow accidentally break memoization or scoped values!
|