Your first component
Let's walk through the creation of your first component fully built with Pinceau.
<style lang="ts">
css({
'.my-button': {
backgroundColor: '{color.blue.1}',
padding: '{space.4}'
}
})
</style>
As you can see in this code, Pinceau uses
<style lang="ts">
blocks and the css()
function.
These blocks are parsed by Pinceau to provide both static and runtime CSS features.
That css()
function calls is not really running in the runtime context of your app, but instead will smartly split between the CSS that can be static, and the features that relies on runtime stylesheet.
By doing that, Pinceau makes you write CSS in the same language as the rest of your component, but does not ship any styling-related JavaScript to the client, unless you ultimately need it.
A simple button
Let's walk together through the creation of a simple button with Pinceau.
For this button, we have multiple requirements, it must:
- Support all existing and future colors from our theme color palette
- Have
sm
,md
,lg
andxl
sizes - Support
--button-primary
and--button-secondary
local tokens for advanced customization - Have type-safe component properties
- Plays nicely with
MDC
syntax from @nuxt/content
Basic styling
First, let's define the basic styling of our button with the
css()
function.
You can find --button-primary
and --button-secondary
, the local tokens for the button palette.
They are then reused in the other properties, as {button.primary}
and {button.secondary}
.
You can also notice some other tokens, like {radii.lg}
, these are coming from the theme.
This example uses @nuxt-themes/tokens
as its tokens definition, but can be adapted to any theme.
<template>
<button class="my-button">
<span>
<ContentSlot :use="$slots.default" unwrap="p" />
</span>
</button>
</template>
<style scoped lang="ts">
css({
'.my-button': {
'--button-primary': `{color.primary.600}`,
'--button-secondary': `{color.primary.500}`,
display: 'inline-block',
borderRadius: '{radii.xl}',
transition: '{transition.all}',
color: '{color.white}',
boxShadow: `0 5px 0 {button.primary}, 0 12px 16px {color.dimmed}`,
span: {
display: 'inline-block',
fontFamily: '{typography.font.display}',
borderRadius: '{radii.lg}',
fontSize: '{fontSize.xl}',
lineHeight: '{lead.none}',
transition: '{transition.all}',
backgroundColor: '{button.primary}',
boxShadow: 'inset 0 -1px 1px {color.dark}',
padding: '{space.3} {space.6}'
},
'&:hover': {
span: {
backgroundColor: `{button.secondary}`,
}
},
'&:active': {
span: {
transform: 'translateY({space.1})'
}
}
}
})
</style>
The button uses <ContentSlot>
instead of <slot>
as it's built to also work with MDC Syntax.
Make it dynamic
That button works fine, but it only supports primary.600
and primary.500
colors.
It's nice because if these tokens gets updated from the tokens.config
file, the colors will follow.
But we also would like our button to support all the colors in our palette, like red
, teal
or blue
.
To achieve that, we'll be using Computed Styles with --button-primary
and --button-secondary
.
<script setup lang="ts">
import type { PinceauTheme } from 'pinceau'
import { computedStyle } from 'pinceau/runtime'
defineProps({
color: computedStyle<keyof PinceauTheme['color']>('red'),
})
</script>
<template>
<button class="my-button">
<span>
<ContentSlot :use="$slots.default" unwrap="p" />
</span>
</button>
</template>
<style scoped lang="ts">
css({
'.my-button': {
'--button-primary': (props) => `{color.${props.color}.600}`,
'--button-secondary': (props) => `{color.${props.color}.500}`,
...previousStyling
}
})
</style>
In that example we added a prop called
color
to our component:
-
computedStyle()
util is a shortcut to define computed styles propskeyof PinceauTheme['color']
will be the type of the prop'red'
will be the default value
PinceauTheme
is a global type that references your theme
We also converted our --button-primary
and --button-secondary
keys to arrow functions.
These functions are how you express Computed Styles in your css()
function.
color
is now a type-safe prop that will follow your color
key in your tokens.config file.
Responsive sizes
Now that our button plays nicely with our color palette, giving it mulitple sizes?
These multiple sizes could be helpful in multiple contexts:
Giving emphasis in certain situations
Fitting for various UI purposes
Being responsive to your Media Queries
Luckily, Variants got you covered for all these usecases and more.
<script setup lang="ts">
import type { PinceauTheme } from 'pinceau'
import { computedStyle } from 'pinceau/runtime'
defineProps({
color: computedStyle<keyof PinceauTheme['color']>('red'),
...variants,
})
</script>
<template>
<button class="my-button">
<span>
<ContentSlot :use="$slots.default" unwrap="p" />
</span>
</button>
</template>
<style scoped lang="ts">
css({
'.my-button': {
...previousStyling
},
variants: {
size: {
sm: {
span: {
padding: '{space.3} {space.6}',
},
},
md: {
span: {
padding: '{space.6} {space.8}'
},
},
lg: {
span: {
padding: '{space.8} {space.12}'
},
},
xl: {
span: {
padding: '{space.12} {space.24}'
},
},
options: {
default: 'sm',
},
},
},
})
</style>
In that example, we added a
variants
key to our css()
function.
We also spread a variants
object in our defineProps()
function.
This added another type-safe prop to our component, called size
.
The size
prop will be usable in two ways:
- As a string prop, accepting the keys inside
size
variant as value:<MyButton size="sm" />
- As an object prop, taking our Media Queries as keys and the keys inside
size
variant as value:<MyButton :size="{ initial: 'sm', lg: 'lg' }" />
Your component is ready
In these few steps, you've learned how to:
Write your style in TypeScript with
<style lang="ts">
andcss()
- Create some local tokens for your component (
--button-primary
,--button-secondary
) - Create bindings between components props and CSS properties with Computed Styles
- Create reusable component appearances variations using Variants