For the complete documentation index, see llms.txt. This page is also available as Markdown.

Component Styling

A standardized approach to building components with Tailwind CSS

Elements uses Tailwind CSS for all component styling. Following this approach ensures consistency across all components—whether built-in, third-party, or custom-made—and makes it easier for users to build sites that are cohesive and simple to update.

Elements compiles Tailwind for you: it scans your templates and the class strings your hooks generate, then builds the final stylesheet. There is no Tailwind config or build step inside a pack.

Your main goal should always be to make a component that feels native to Elements.

The built-in components are Tailwind 4 ready, and every example on this page works with Tailwind 4. Themes declare the Tailwind version they use by setting tailwindVersion to v4 in their info.json—see What are Themes?

Why Tailwind?

Tailwind offers a utility-first approach to styling. Instead of writing custom CSS, you apply small, reusable utility classes directly to your HTML elements. This approach provides several benefits:

  • Consistency: All components share the same styling vocabulary

  • No style leakage: Styles applied via utility classes stay within each component's HTML structure

  • Optimized output: Elements generates only the CSS your page actually uses, resulting in smaller, more efficient stylesheets

  • Theme integration: Utility classes map directly to Theme Studio values

Theme-Driven Design

In Elements, styling configurations—colors, fonts, spacing, and more—are controlled by the Theme and managed through the Theme Studio. Components inherit styles from the theme rather than dictating their own.

This means:

  • Users can switch themes without breaking their site's design

  • All components "just work" within any project

  • Styling remains consistent across the entire site

For example, a Heading component might define its default font family as heading and font size as 3xl. The component doesn't care what specific font or pixel size these represent—it only needs to know the values exist. The Theme Studio handles the actual values, allowing full customization while maintaining compatibility.

Theme UI Controls

When designing your component, use Theme-based UI Controls wherever possible. These controls integrate directly with the Theme Studio, ensuring your component respects the user's theme settings.

Control
Purpose

Theme Border Width

Border thickness values

Theme Border Radius

Corner rounding values

Theme Color

Color palette selection

Theme Font

Font family selection

Theme Spacing

Margin and padding values

Theme Shadow

Box shadow styles

Theme Text Style

Text size presets

Theme Typography

Typography class selection

Theme Colors

All components need to work out of the box. Every Elements theme defines a set of semantic colors—use these rather than hard-coded palette colors so your component adapts to any theme:

Color
Usage

brand

Dominant color for branding and key interactive items like buttons and links

accent

Complementary color for secondary elements and supporting details

surface

Background colors that provide contrast behind text and elements

text

Headings and body text

black / white

Pure black and pure white

Each color comes in Tailwind's standard 50950 brightness scale, and themes also include all of the default Tailwind color palettes. A Theme Color control resolves to a name-brightness token such as surface-500, ready to drop into a utility class. See What are Themes? for the full list of tokens a theme provides.

Applying Utilities Directly in Templates

For styling that doesn't need to be user-configurable, apply Tailwind utilities directly in your template files:

This approach eliminates the need for scoped CSS—each component's styles are self-contained within its HTML structure.

Generating Tailwind Classes with Format

Use the format key in your properties.json to transform property values into Tailwind utility classes. This keeps your templates clean and ensures proper class output.

In your template, reference the property directly:

This outputs:

This is exactly how the built-in components work. The core Border Color control formats a Theme Color as a border utility:

{{borderColor}} outputs border-surface-500.

The same pattern works with any theme control. The core Button component uses a Theme Spacing control to set the gap between its icon and text:

{{dropzoneSpacing}} outputs gap-x-2.

The format key works with any control type, including arbitrary values like opacity-[{{value}}%]. See Format for more examples.

Responsive Styling

Elements supports responsive styling out of the box. Theme controls can define different values for each breakpoint using the standard Tailwind breakpoint prefixes.

This outputs responsive classes that apply at each breakpoint:

Supported breakpoints follow Tailwind's defaults:

Prefix
Minimum Width

base

0px (default)

sm

640px

md

768px

lg

1024px

xl

1280px

2xl

1536px

Composing Classes in hooks.js

Because formatted controls emit ready-made utility classes, most real components do their styling in hooks.js: the hook gathers the class strings from rw.props, combines them with static classes and conditional logic, and applies the result to the component's root element. Templates stay clean because they never see the full class list.

Here's the pattern, simplified from the core Button component. It uses the buttonColor and dropzoneSpacing controls defined earlier on this page, and only plain JavaScript—paste it into a new project and it works:

A few things to notice:

  • Static classes (flex items-center cursor-pointer) sit alongside property-driven classes (buttonColor is already bg-brand-500 and dropzoneSpacing is already gap-x-2—formatted by their controls).

  • Conditional logic decides which classes apply—wantsDropzone && dropzoneSpacing only includes the gap when the icon dropzone is enabled, and .filter(Boolean) drops any false or empty entries before joining.

  • rw.setRootElement() applies the finished class string to the component's root element.

  • Small presentational values pass to the template via rw.setProps().

The template only handles content and the few props it needs—this is the entire Button template:

The built-in components compose their classes exactly this way, but at a larger scale they use shared helpers—classnames(), advancedClasses(), and the global* family (globalLayout, globalBorders, and friends)—that ship with the Build Tools and are bundled into hooks.js at build time. Plain arrays and join(" ") need no build step; reach for the Build Tools when your pack grows. See classnames and globalBorders for details.

Combining Property Values

Hooks can merge multiple formatted controls into a single utility. The core border controls pair a Theme Color ("format": "border-{{value}}") with an opacity slider ("format": "[{{value}}%]"), then join them with Tailwind's slash modifier:

The split/map handles Theme Colors with separate light and dark values, which emit two space-separated classes (for example border-surface-200 dark:border-surface-800)—each one needs the opacity applied.

Multi-Layer Components

More complex components style several elements, not just the root. The convention used throughout the core pack is to build a classes object in the hook—one class string per layer—and pass it to the template with rw.setProps().

Simplified from the core Container component, which renders separate background and content layers stacked in a one-cell grid:

The template applies each layer's classes where they belong:

The wrapper is a one-cell grid, and [&>*]:col-start-1 [&>*]:row-start-1 places every child in that same cell so the background, content, and any overlay stack on top of each other. Splitting the layers means borders, backgrounds, and effects can each target the right element independently.

Advanced Tailwind Techniques

The core components lean on a handful of modern Tailwind features worth knowing about.

Named Groups for Scoped Hover States

Every core component adds group/${id} to its root, where id is the component instance's unique identifier from rw.node. Child elements—or the hook's generated classes—can then use group-hover/${id}: variants that respond only to that instance:

Because the group name is unique per instance, hover states never leak between two copies of the same component on a page. The same idea works between sibling elements: the Container's content layer carries the peer class so the background layer can respond to it with peer-hover: variants. See getHoverPrefix for the helper the core pack uses to pick the right variant.

Arbitrary Variants

Arbitrary variants style descendants your template doesn't render directly. The core Text component applies user-chosen link styles to every <a> inside its rich text this way:

The same technique handles pseudo-states on those descendants ([&_a:hover]:, [&_a:visited]:) and direct children ([&>*]:), as seen in the Container's grid stacking above.

Data-Attribute Variants

State-dependent styling doesn't need JavaScript to toggle classes—use data-attribute variants and flip the attribute instead. The core border helper emits classes like:

The styles apply automatically whenever the element's data-active attribute is "true".

Dark Mode

A Theme Color control with darkName/darkBrightness set emits a pair of classes, for example bg-surface-50 dark:bg-surface-800. When a hook adds another variant on top of such a value, the new prefix must land after dark:[&_a]:text-brand-500 dark:[&_a]:text-brand-50, not [&_a]:dark:…. The Build Tools include injectPrefixOnDarkModeColors to handle this rewrite for you.

Supporting User-Defined Classes

Give power users an escape hatch. Every core component exposes an Advanced group in the inspector with a free-text CSS Classes control, and appends its value to the root class list:

The Build Tools ship a ready-made version of this helper—see advancedClasses.

Third-party components should follow the same convention so users can add their own utility classes to any component without editing templates.


Custom CSS (When Necessary)

While Tailwind is the recommended approach, there are situations where custom CSS may be required. Use these techniques sparingly, as custom CSS will not be controllable from the Theme Studio.

Using the Component ID for Scoping

You can scope CSS to a specific component instance using the {{id}} variable, which outputs a unique identifier for each component. CSS files placed in your component's templates/ directory are processed through the template engine—see CSS Templates for full details.

In your CSS template file:

In your HTML template:

BEM Naming Convention

If you need multiple custom classes, use BEM (Block Element Modifier) naming with a unique prefix to avoid conflicts:

CSS Custom Properties

For values that need to be dynamic, use CSS custom properties set via inline styles:

This approach lets users control CSS values through the inspector while keeping the styling logic in CSS.

Theme Colors in Custom CSS

With a Tailwind 4 theme (tailwindVersion: "v4"), every theme color is also exposed as a CSS custom property under the --color-* namespace. A Theme Color control formatted as var(--color-{{value}}) can then be used directly in your CSS—ideal for gradient stops, SVG fills, or overriding third-party library styles while keeping the color choice in Theme Studio. See Using Tailwind 4 CSS Custom Properties.

Last updated

Was this helpful?