Learn-skills.dev canvas-styling-conventions

Technology stack

install
source · Clone the upstream repo
git clone https://github.com/NeverSight/learn-skills.dev
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/acquia/nebula/canvas-styling-conventions" ~/.claude/skills/neversight-learn-skills-dev-canvas-styling-conventions && rm -rf "$T"
manifest: data/skills-md/acquia/nebula/canvas-styling-conventions/SKILL.md
source content

Technology stack

TechnologyPurpose
Tailwind CSS 4.1+Styling
class-variance-authority (CVA)Component variants
clsx
+
tailwind-merge
via
cn()
Class name merging

Only use these dependencies for styling. Do not add third-party CSS libraries or create new styling utilities.

Styling conventions

  • Use Tailwind's theme colors (
    primary-*
    ,
    gray-*
    ) defined in
    global.css
    .
  • Avoid hardcoded color values; use theme tokens instead.
  • Follow the existing focus, hover, and active state patterns from examples.

The cn() utility

Use

cn()
to merge Tailwind classes. It combines
clsx
for conditional classes with
tailwind-merge
to resolve conflicting utilities. Import from either source:

import { cn } from "@/lib/utils";
// or
import { cn } from "drupal-canvas";

Example usage:

const Button = ({ variant, className, children }) => (
  <button
    className={cn(
      "rounded px-4 py-2",
      variant === "primary" && "bg-primary-600 text-white",
      variant === "secondary" && "bg-gray-200 text-gray-800",
      className,
    )}
  >
    {children}
  </button>
);

Accept className for style customization

Every component should accept a

className
prop to allow style overrides. Pass it to
cn()
as the last argument so consumer classes take precedence.

const Card = ({ colorScheme, className, children }) => (
  <div className={cn(cardVariants({ colorScheme }), className)}>{children}</div>
);

className
is an implementation/composition prop, not an editor prop. Do not add
className
to
component.yml
, do not mark it as required, and do not surface it in Canvas metadata.

Tailwind 4 theme variables

Canvas projects use Tailwind CSS 4's

@theme
directive to define design tokens in
global.css
. Variables defined inside
@theme { }
automatically become available as Tailwind utility classes.

Always check

global.css
for available design tokens. The
@theme
block is the source of truth for colors, fonts, breakpoints, and other design tokens.

How theme variables map to utility classes

When you define a CSS variable in

@theme
, Tailwind 4 automatically generates corresponding utility classes based on the variable's namespace prefix:

CSS Variable in
@theme
Generated Utility Classes
--color-primary-600: #xxx
bg-primary-600
,
text-primary-600
,
border-primary-600
--color-gray-100: #xxx
bg-gray-100
,
text-gray-100
,
border-gray-100
--font-sans: ...
font-sans
--breakpoint-md: 48rem
md:
responsive prefix

The pattern is:

--{namespace}-{name}
becomes
{utility}-{name}
.

Examples

Given this definition in

global.css
:

@theme {
  --color-primary-600: #1899cb;
  --color-primary-700: #1487b4;
}

You can use these colors with any color-accepting utility:

// Correct
<button className="bg-primary-600 hover:bg-primary-700 text-white">
  Click me
</button>

// Wrong
<button className="bg-[#1899cb] text-white hover:bg-[#1487b4]">Click me</button>

Arbitrary values (e.g.,

bg-[#xxx]
) are acceptable for rare, one-off cases where adding a theme variable would be overkill. However, if a color appears in multiple places or represents a brand/design system value, add it to
@theme
instead.

Semantic aliases

Theme variables can reference other variables to create semantic aliases:

@theme {
  --color-primary-700: #1487b4;
  --color-primary-dark: var(--color-primary-700);
}

Both

bg-primary-700
and
bg-primary-dark
will work. Use semantic aliases when they better express intent (e.g.,
primary-dark
for a darker brand variant).

Adding or updating theme variables

When a design requires a color, font, or other value not yet defined in the theme, add it to the

@theme
block in
global.css
rather than hardcoding the value in a component.

When to add new theme variables:

  • A design introduces a new brand color or shade
  • You need a semantic alias for an existing value (e.g.,
    --color-accent
    )
  • The design uses a specific spacing, font, or breakpoint value repeatedly

When to update existing theme variables:

  • The brand colors change (update the hex values)
  • Design tokens need adjustment across the system

Example - adding a new color:

@theme {
  /* Existing tokens */
  --color-primary-600: #1899cb;

  /* New token for a success state */
  --color-success: #22c55e;
  --color-success-dark: #16a34a;
}

After adding, you can immediately use

bg-success
,
text-success-dark
, etc.

Keep the theme organized. Group related tokens together with comments explaining their purpose. Follow the existing naming conventions in

global.css
(e.g., numbered shades like
primary-100
through
primary-900
, semantic names like
primary-dark
).

Color props must use variants, not color codes

Never create props that allow users to pass color codes (hex values, RGB, HSL, or any raw color strings). Instead, define a small set of human-readable variants using CVA that map to the design tokens in

global.css
.

Always check

global.css
for available design tokens. The tokens defined there (such as
primary-*
,
gray-*
, etc.) are the source of truth for color values.

Wrong - allowing raw color values:

# Wrong
props:
  properties:
    backgroundColor:
      title: Background Color
      type: string
      examples:
        - "#3b82f6"
// Wrong
const Card = ({ backgroundColor }) => (
  <div style={{ backgroundColor }}>{/* ... */}</div>
);

Correct - using CVA variants with design tokens:

# Correct
props:
  properties:
    colorScheme:
      title: Color Scheme
      type: string
      enum:
        - default
        - primary
        - muted
        - dark
      meta:enum:
        default: Default (White)
        primary: Primary (Blue)
        muted: Muted (Light Gray)
        dark: Dark
      examples:
        - default
// Correct
import { cva } from "class-variance-authority";

const cardVariants = cva("rounded-lg p-6", {
  variants: {
    colorScheme: {
      default: "bg-white text-black",
      primary: "bg-primary-600 text-white",
      muted: "bg-gray-100 text-gray-700",
      dark: "bg-gray-900 text-white",
    },
  },
  defaultVariants: {
    colorScheme: "default",
  },
});

const Card = ({ colorScheme, children }) => (
  <div className={cardVariants({ colorScheme })}>{children}</div>
);

This approach ensures:

  • Consistent colors across the design system
  • Users select from curated, meaningful options (not arbitrary values)
  • Easy theme updates by modifying
    global.css
    tokens
  • Better accessibility through tested color combinations