Skip to main content

Paperclip Syntax

Basics

You can use regular HTML & CSS in Paperclip. For example:

<style>
div {
color: purple;
font-size: 32px;
font-family: papyrus;
}
</style>
<div>I'm purple!!</div>

The big difference between Paperclip and regular HTML & CSS is that CSS in Paperclip is scoped. This means that styles won't leak into other documents.

You can define global CSS, but you need to be explicit about that using the :global() selector. You can also override styles into other documents using selector reference syntax. You can also use styles defined within another document by using the @export or inject-styles.

Styling

Nested rules

Nested rules eliminates some redundancy around defining style selectors.

Syntax:

.parent-rule {

/* equivalent to: .parent-rule .child-rule */
.child-rule {

}
/* equivalent to: .parent-rule--variant */
&--variant-rule {

}
}

Example:

// file: nested-style-demo.pc
<style>
.container {
.header {
font-size: 32px;
font-weight: 600;
color: red;
}
.content {
font-size: 18px;
color: blue;
}
}
</style>

<div class="container">
<div class="header">
Header
</div>
<div class="content">
content
</div>
</div>

The & token can be used to combine the parent selector in nested rules like so:

// file: nested-combine-demo.pc
<style>
.button {
font-family: Noteworthy;
background: grey;
display: inline-block;
border: 2px solid grey;
border-radius: 2px;
padding: 2px 10px;
&--secondary {
color: grey;
background: transparent;
}
&.preview {
margin: 10px;
}
}
</style>

<div class="button preview">
Button primary
</div>
<div class="button button--secondary preview">
Button secondary
</div>

Also note that you can nest @media queries like so:

div {
@media screen and (max-width: 400px) {
color: blue;
}
}

Element scoping

Style blocks that are the defined within elements are scoped to that element. For example:

<div>
<div>
<style>
color: red;
span {
color: blue;
}
</style>
I'm red text!
<span>I'm blue text!</span>
</div>
I'm black text
</div>

Declarations defined directly in the style elements like the example above are applied to the parent element.

Scoped styles are recommended since they keep your styles & elements together in one spot, which makes them a bit more maintainable. It also provides you an easier way to know exactly what's being styled at a glance.

:within(ancestor-selector)

:within([ancestor-selector]) allows you to apply styles within a parent or ancestor.

<div class="bolder">
<div>
<style>

/* this style block is applied if a
parent / ancestor has .bolder class */
&:within(.bolder) {
font-weight: 600;
}
</style>
Some text
</div>
</div>

@mixin

Style mixins are useful for defining a bundle of style declarations (like color, font-size) that you then can include into style rules.

Syntax:

@mixin mixin-name {
/* style props */
decl-name: decl-value;

/* nested rules */
div {
color: blue;
}

/* takes body of include statement */
@content;
}

Including mixins syntax:

.my-style {
@include mixin-name;

@include mixin-with-content {
display: block;
div {
color: blue;
}
}
}

Example:

<style>
:global(:root) {
--font-family-default: Quotes Script;
--color-grey-100: #333;
--color-green-100: #0C0;
}

@mixin color-text-green {
color: var(--color-green-100);
}

@mixin default-text {
font-family: var(--font-family-default);
color: var(--color-grey-100);
font-size: 32px;
}

.message {

/* @include includes style mixins; you can have any number of them separated by spaces. */
@include default-text;
@include color-text-green;
text-decoration: underline;
}
</style>

<div class="message">
What has to be broken before you can use it?
</div>

Re-using media queries

Media queries are re-usable in Paperclip by using the following pattern:

@mixin desktop {
@media screen and (max-width: 1400px) {
@content;
}
}

div {
@include desktop {
font-size: 24px;
}
}

Example:

// file: main.pc
<import src="./breakpoints.pc" as="breakpoints" />

<!--
@frame { visible: false }
-->
<div component as="App">
<style>
font-family: sans-serif;
@include breakpoints.desktop {
font-size: 18px;
}
@include breakpoints.mobile {
font-size: 72px;
}
</style>
{children}
</div>

<!--
@frame { title: "App / Desktop", width: 1050, height: 421, x: 89, y: -295 }
-->
<App>
I'm some content
</App>

<!--
@frame { title: "App / Mobile", width: 326, height: 507, x: 102, y: 261 }
-->
<App>
I'm some content
</App>

// file: breakpoints.pc
<style>
@export {
@mixin desktop {
@media screen and (max-width: 1400px) {
@content;
}
}
@mixin mobile {
@media screen and (max-width: 400px) {
@content;
}
}
}
</style>

@export

The @export rule allows you to export styles to other documents, as well as application code.

Syntax:

@export {
.my-style {
/* styles here */
}

@keyframes my-keyframe {
/* keyframe code here */
}

@mixin my-mixin {
/* styles here */
}
}

Example:

// file: main.pc
<import src="./styles.pc" as="styles" />


<!-- $ is a class reference - docs below -->
<div class="$styles.default-text">
<style>
@include styles.big-text;
animation: styles.pulse 1s infinite;
</style>

Hello again!
</div>

// file: styles.pc

<style>

/* Exported mixins */

/* @export docs below */
@export {
@mixin text-color-green-default {
color: green;
}
@mixin big-text {
font-size: 32px;
}
}

/* Exported classes */

@export {
.default-text {
font-family: Herculanum;
letter-spacing: 0.05em;
}
}

/* Exported animations */

@export {
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
}
</style>

Note that you must wrap styles around @export if you want to reference them.

On that note, I'd recommend only exporting things that you need in other documents since export keywords (@export, export) make it clear around what's public & private.

You can reference class names in React code like so:

import * as cx from "classnames";
import * as typography from "./typography.pc";
<div className={cx(
typography.classNames["default-text"]
)}>

Note that .header-text is not exported, so it's not available in our app code.

$class-reference

Paperclip allows you to explicitly reference class selectors, which is helpful if you're looking to reference or overrides styles in other documents.

Syntax:

<div class="$class-name" />

<div class="$imported-doc.class-name" />

Example:

// file: main.pc
<import src="./atoms.pc" as="atoms" />

<span class="$atoms.font-default">
Hello
</span>
// file: atoms.pc

<style>
@export {
.font-default {
font-family: Helvetica;
color: blue;
font-size: 32px;
letter-spacing: 0.05em;
}
}
</style>

You can also use class references to override component styles.

:global

All style rules are scoped by default to the document they're defined in. This ensures that they don't leak & have unintended side-effects. However, there are rare cases when you may need to define a global style rule, such as styling HTML defined outside of Paperclip that doesn't have a way to define a class attribute.

Syntax:

:global(.my-selector-here > div ~ .another-selector) {
name: value;
}

Here's an example that stylizes parts of react-select:

<style>

.wrapper {

/* global for now so that we get tests to pass */
:global(.select__) {
&control {
display: flex;
background: var(--color-background);

/* more declarations here */

&--is-focused {
/* more declarations here */
}

&:hover {
/* more declarations here */
}
}
&value-container,
&single-value,
&multi-value__label,
&input {
/* more declarations here */
}

/* selectors here */
}
}
</style>

<div export component as="Wrapper" class="wrapper">
{children}
</div>

Here's how you use the above styles in React code:

import * as ui from './Select.pc';

// Keep the select styles locked in
<ui.Wrapper>
<DynamicSelect classNamePrefix="select" {...props} />
</ui.Wrapper>

Try to avoid :global selectors whenever possible since they leak into other documents, and may result in unintended side-effects. If you need to use :global, try to wrap it around a style rule that's scoped to the document. For example:

/* Safer to use */
.container {
:global(body) {

}
}

Import

You can import styles & components from other files.

Syntax:

<import src="./path/to/document.pc" as="unique-namespace" />

Example:

// file: main.pc
<import src="./pane.pc" as="pane" />
<import src="./atoms.pc" as="atoms" />

<pane.Container>
<pane.Header>
<span class="$atoms.font-big">
Header content
</span>
</pane.Header>
<pane.Content>
Some content
</pane.Content>
</pane.Container>


// file: pane.pc
<import src="./atoms.pc" as="atoms" />
<style>
@mixin padded {
margin: 0px 8px;
}
</style>

<div export component as="Container" class="Container">
<style>
@include atoms.font-default;
</style>
{children}
</div>

<div export component as="Header" class="Header">
<style>
@include padded;
font-size: 18px;
font-weight: 600;
</style>
{children}
</div>

<div export component as="Content" class="Content">
<style>
@include padded;
</style>
{children}
</div>

<!-- Preview -->

Nothing here!

// file: atoms.pc

<style>
@export {
@mixin font-default {
font-family: Helvetica;
}
.font-big {
@include font-default;
font-size: 24px;
}
}
</style>

The import as keyword defines a namespace that you can use to access exported properties defined within other documents, like above.

Other examples:

inject-styles

The inject-styles props injects all of the import's exported styles into the current document. For example:

<import src="./tailwind.css" inject-styles />

<div class="font-sans">
Styles from tailwind.css
</div>

This is particularly useful for third-party CSS since inject-styles includes all CSS selectors (class, ID, element, etc) into the current scope. Note that injected styles are only applied to the current document, so if you're importing components from another file, those components won't be styled. For example:

<import src="./tailwind.css" inject-styles />
<import src="./some-module.pc" as="module" />

<div class="font-sans">
Styles from tailwind.css

<!-- injected styles are NOT applied to this element -->
<module.Test />
</div>

Note that .css files are a special case since all selectors are automatically exported. If you want to inject styles from a .pc file, you'll need to explicitly export the styles that you'd like to inject. For example:

<style> 
@export {
* {
box-sizing: border-box;
}

.font-large {
font-size: 24px;
}
}

.this-class-is-not-applied {
color: orange;
}
</style>

👆 everything that is defined within export can be injected into a document. Here's how you use the above example:

<import src="./my-module.pc" inject-styles />
<div class="font-large">
I'm large text
</div>

Components

Components are your UI building blocks. Just add a component attribute to a root element (doesn't have a parent).

Syntax:


<!-- defining the component -->
<element-name component as="my-component-name">
</element-name>

<!-- using it -->
<my-component-name />

Example:


<!-- class and class can be used interchangeably -->
<!--
@frame { visible: false }
-->
<div component as="Message" class="Message">
<style>
font-family: Comic Sans MS;
font-size: 32px;
color: #F0F;
</style>
{children}
</div>

<!-- Preview section -->

<Message>
Hooray!
</Message>

Exporting components

Components can be exported to be used in application code, as well as other documents.

Syntax:


<!-- just add the "export" attribute to any component -->
<div export component as="MyComponent">
</div>

Example:

// file: todos.pc
<import src="./styles.pc" as="styles" />

<!-- Components -->

<!--
@frame { visible: false }
-->
<div export component as="App" class="$styles.App">
{children}
</div>

<!--
@frame { visible: false }
-->
<input export component as="NewItemInput" {onChange} />

<!--
@frame { visible: false }
-->
<div export component as="Header" class="$styles.Header">
<h4>Todos</h4>
{children}
</div>

<!--
@frame { visible: false }
-->
<ul export component as="List" class="$styles.List">
{children}
</ul>

<!--
@frame { visible: false }
-->
<li export component as="Item" class="$styles.Item" {onClick}>
<input type="checkbox" checked={completed} />
<span class="$styles.label">{children}</span>
</li>

<!-- Preview -->

<App>
<Header>
<NewItemInput />
</Header>
<List>
<Item>Wash car</Item>
<Item>Wash car</Item>
<Item completed>Wash car</Item>
</List>
</App>

// file: styles.pc

<!-- Typically in the same file as components, but they're here for this demo since they're not the focus. -->
<style>
@export {
.App {
font-family: Chalkduster;
}
.Header {
h4 {
margin: 0;
margin-bottom: 8px;
}
margin-bottom: 8px;
}
.List {
margin: 0;
padding: 0;
list-style-type: none;
}
.Item {

// Needs
:global(input[type="checkbox"]) {
margin-right: 8px;
&:checked ~ .label {
text-decoration: line-through;
}
}
}
}
</style>

Here's how we can use this in our React app:

import React, { useRef, useState } from "react";
import * as ui from "./todos.pc"

const TodoApp = () => {
const [todos, setTodos] = useState([
{ completed: true, label: "walk dog" },
{ completed: true, label: "take out trash" }
]);

const onNewInputChange = (event) => {
// code to add new todo item here
}

return <ui.App>
<ui.Header>
<NewItemInput onChange={} />
<ui.List>
{todos.map(({completed, label}, i) => (
<ui.Item
onClick={/* toggle completed handler here */}
completed={completed}>
{label}
<ui.Item>
))}
</ui.List>
</ui.Header>
</ui.App>;
}

We can also use our exported component in other Paperclip documents. Here's an example:

// file: importing-components-demo.pc
<import src="./todos.pc" as="todos" />

<style>
.preview {
display: flex;
&-item {
margin-left: 20px;
}
}
</style>

<div class="preview">
<todos.Preview class="$preview-item" />
<todos.EmptyPreview class="$preview-item" />
</div>

// file: todos.pc
<import src="./styles.pc" as="styles" />

<!-- Components -->

<!--
@frame { visible: false }
-->
<div export component as="App" class="$styles.App {class?}">
{children}
</div>

<!--
@frame { visible: false }
-->
<input export component as="NewItemInput" {onChange} />

<!--
@frame { visible: false }
-->
<div export component as="Header" class="$styles.Header">
<h4>Todos</h4>
{children}
</div>

<!--
@frame { visible: false }
-->
<ul export component as="List" class="$styles.List">
{children}
</ul>

<!--
@frame { visible: false }
-->
<li export component as="Item" class="$styles.Item" {onClick}>
<input type="checkbox" checked={completed} />
<span class="$styles.label">{children}</span>
</li>

<!-- Export re-usable previews that can be used
in other previews -->

<!--
@frame { visible: false }
-->
<App export component as="Preview" {class?}>
<Header>
<NewItemInput />
</Header>
<List>
<Item>Clean cat car</Item>
<Item>Wash car</Item>
<Item completed>Wash car</Item>
</List>
</App>


<!--
@frame { visible: false }
-->
<App export component as="EmptyPreview" {class?}>
<Header>
<NewItemInput />
</Header>
Nothing to see here
</App>

<!-- render main preview -->
<Preview />

// file: styles.pc

<!-- Typically in the same file as components, but they're here for this demo since they're not the focus. -->
<style>
@export {
.App {
font-family: Chalkduster;
}
.Header {
h4 {
margin: 0;
margin-bottom: 8px;
}
margin-bottom: 8px;
}
.List {
margin: 0;
padding: 0;
list-style-type: none;
}
.Item {

// Needs
:global(input[type="checkbox"]) {
margin-right: 8px;
&:checked ~ .label {
text-decoration: line-through;
}
}
}
}
</style>

Overriding component styles

You can override styles in other components assuming that a component exposes an attribute that's bound to class.

Syntax:

attributeBoundToClassName="$class-name"

Example:

// file: style-override-demo.pc
<import as="Message" src="./message.pc" />
<style>
.my-style-override {
text-decoration: underline;
}
</style>
<Message class="$my-style-override">
Hello World
</Message>

// file: message.pc

<!--
@frame { visible: false }
-->
<div export component as="default" class="{class?}">
<style>
font-size: 24px;
font-family: Helvetica;
</style>
{children}
</div>

Check out class references for more information on how to use $.

Using scoped styles

Alternatively, you can overriding your components appearance by using scoped styles like so:


<!-- Note that class is still necessary here! -->
<!--
@frame { visible: false }
-->
<div export component as="Message" {class?}>
<style>
font-family: sans-serif;
color: red;
</style>
{children}
</div>

<Message>
<style>
color: blue;
</style>

I'm blue text!
</Message>

Note that you need to provide a class in your component for inline style overrides to work.

☝🏻 this approach keeps your overrides together, which can be a bit easier to read. The other benefit to this approach is that your code remains portable since everything's in one spot.

Changing the tag name

You may want to change the native tag name of a component. An example of this is a Button component that may be a button or a tag.

Syntax

<button export component as="Button" {tagName?}>
{children}
</button>

Example

// file: demo.pc 

<!--
@frame { visible: false }
-->
<input component as="Input" {tagName?} {placeholder} />

<div>
<Input placeholder="I'm a text input" />
<Input tagName="textarea" placeholder="I'm a text area" />
</div>

Bindings

Bindings allow you to define dynamic behavior in components.

Child bindings

Syntax:

<div component as="MyComponent">

<!-- reserved keyword - takes element children. -->
{children}

<!-- can be defined via attributes -->
{anotherSlot}
</div>

Example:


<!--
@frame { visible: false }
-->
<h1 component as="Header">
{children}
</h1>

<Header>
I'm a header
</Header>

There will probably be the case where you want to define multiple areas of a component for children to go into. Here's an example of that:

// file: main.pc
<import src="./styles.pc" as="styles" />

<!--
@frame { visible: false }
-->
<div component as="Pane" class="$styles.Pane">
<div class="$styles.header">
<div class="$styles.title">{title}</div>
<div>{controls}</div>
</div>
<div class="$styles.content">
{children}
</div>
</div>

<div component as="AddButton">
+
</div>

<Pane title={<strong>My header</strong>} controls={<AddButton />}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</Pane>

// file: styles.pc

<!-- keeping this in another file so that it's not the center of attention -->
<style>
@export {
.Pane {
font-family: Courier;
width: 200px;
color: #333;
.header, .content {
padding: 4px 8px;
}
.header {
background: #C0C0C0;
display: flex;
.title {
flex-grow: 1;
}
}
.content {
background: #CFCFCFCF;
padding: 4px;
}
}
}
</style>

{title} and {controls} (and technically also {children}) are considered slots for child nodes to go into, and they can be filled in via attributes:

<Pane 
title={<span>some title</span>}
controls={<button>A button</button>}>
Content children
</Pane>

Attribute bindings

You can define dynamic attributes on your elements. For example:

// file: buttons.pc
<import src="./typography.pc" as="typography" />
<import src="./styles.pc" as="styles" />

<!-- Components -->

<!-- Generally I'd recommend just a `{class}` binding instead of `{customClassName}` class name, which I'm only using here to make more clear around how it works. -->
<!--
@frame { visible: false }
-->
<div component as="Button"
class="$styles.button {customClassName}">
{children}
</div>


<Button customClassName="$typography.big-text $typography.strong">
Button
</Button>

// file: styles.pc
<import src="./typography.pc" as="typography" />
<style>
@export {
.button {
color: red;
@include typography.default-text;
}
}
</style>

// file: typography.pc
<style>
@export {
@mixin default-font {
font-family: Helvetica;
}
@mixin default-text {
@include default-font;
font-size: 18px;
color: #333;
}
.big-text {
@include default-font;
font-size: 32px;
font-weight:
}
.strong {
font-weight: 800;
}
}
</style>

Bindings can also be defined outside of string attributes. For example:

<div component as="Test">
<span class="title" ref={spanRef}>
{title}
</span>
{children}
</div>

Ref here is specific to React around referencing DOM nodes.

You can also use the shorthand approach like so:

<div component as="Test" {ref}>
<span class="title">
{title}
</span>
{children}
</div>

This is particularly useful for making your code more DRY. For example:

<input export component as="Input" 
{onChange?}
{defaultValue}
{value}
/>

Optional bindings

By default, bindings are required. So if you define {class} on an element, that property will be required when compiled into application code. To make a binding optional, just add a ? after the binding name like so:

<div component as="Message" {class?}>
{children}
</div>

☝🏻 Here, class is optional, whereas children is not. When compiled to TypeScript, here's what you get:

/* other generated code here */

type MessageProps = {
class?: Function,
children: ReactNode,
};

export const Message: React.FC<MessageProps>;

Variant styles

The variant style syntax allows you to conditionally apply styles. For example:

Syntax:

<div component as="MyComponent" class:variant-name="class-name">
</div>

<!-- defining variant-name will apply class-name style -->
<MyComponent variant-name />

Example:


<!--
@frame { visible: false }
-->
<div component as="Header"
class:big="big"
class:medium="medium"
class:small="small">

<style>
font-family: Luminari;
font-size: 12px;

/* I recommend that you do this instead of &.big to avoid
CSS specificity issues */
&.big {
font-size: 32px;
}
&.medium {
font-size: 18px;
}
&.small {
font-size: 12px;
}

</style>

{children}
</div>

<div>
<Header big>
Big header
</Header>
<Header medium>
Medium header
</Header>
<Header small>
Small header
</Header>
<Header>
Regular header
</Header>
</div>

Fragments

Fragments are useful if you want to render a collection of elements. For example:

<!--
@frame { visible: false }
-->
<ul component as="List">
{listItems}
</ul>

<List
listItems={<fragment>
<li>feed fish</li>
<li>feed cat</li>
<li>feed me</li>
</fragment>}
/>

You can also define components from them:

<fragment component as="Items">
<li>Item</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</fragment>

<ul>
<Items />
</ul>

Annotations

Annotations allow you to specify additional metadata about your elements -- this is used particularly for documentation & other visual helpers when developing your UIs.

@frame

The @frame annotation allows to you to specify preview frame dimensions for your element. For example:

x/y/width/height dimensions can be specified visually in the preview window.

To hide frames from rendering, you can specify visible: false like so:

<!-- 
@frame { visible: false }
-->
This frame isn't visible in the preview