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.
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.
Border thickness values
Corner rounding values
Color palette selection
Font family selection
Margin and padding values
Box shadow styles
Text size presets
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:
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 50–950 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:
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 (buttonColoris alreadybg-brand-500anddropzoneSpacingis alreadygap-x-2—formatted by their controls).Conditional logic decides which classes apply—
wantsDropzone && dropzoneSpacingonly includes the gap when the icon dropzone is enabled, and.filter(Boolean)drops anyfalseor 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:
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.
Related Documentation
Building Complex Components - Guides applying these styling patterns in real components
Responsive Images & Media - Breakpoint-keyed classes and dynamic CSS
Tutorial: Build an FAQ Component - Class composition applied step by step
Last updated
Was this helpful?

