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 and xl 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
<template> usage
<MyButton color="primary" size="md">
Hello world ๐Ÿง‘โ€๐ŸŽจ
</MyButton>
MDC usage
::my-button{color="primary" size="md"}
Hello world ๐Ÿง‘โ€๐ŸŽจ
::

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.

MyButton.vue
<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 props
    • keyof 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.

<template> usage
<MyButton color="red">
Red button
</MyButton>
<MyButton color="teal">
Teal button
</MyButton>
<MyButton color="blue">
Blue button
</MyButton>
MDC usage
::my-button{color="red"}
Red button
::
::my-button{color="teal"}
Teal button
::
::my-button{color="blue"}
Blue button
::

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' }" />
<template> usage
<MyButton size="sm">
Small button
</MyButton>
<MyButton size="md">
Medium button
</MyButton>
<MyButton :size="{ initial: 'sm', md: 'md', lg: 'lg', xl: 'xl' }">
Responsive button
</MyButton>
MDC usage
::my-button{size="sm"}
Small button
::
::my-button{size="md"}
Medium button
::
::my-button
---
size:
initial: sm
md: md
lg: lg
xl: xl
---
Responsive button
::

Your component is ready

In these few steps, you've learned how to:

  • Write your style in TypeScript with <style lang="ts"> and css()
  • 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