Frontile components can be customized with Tailwind Variants, either globally or per-instance. This guide covers both approaches.
Frontile uses Tailwind Variants to manage and customize component styles. Tailwind Variants provide a structured way to define different visual representations (or variants) for components while keeping your styles consistent and maintainable.
A slot represents a specific part of a component that can be styled or customized separately. Slots allow you to apply different styles to different parts of a complex component, providing more granular control.
When customizing styles in Frontile, you can either apply styles globally using registerCustomStyles or customize individual components using class arguments. You can override default component styles by passing your own class names to the class or classes argument, depending on whether the component has slots.
data-component and data-partEvery Frontile component renders a stable, documented DOM anatomy, independent of its CSS classes. Two attributes carry it:
data-component="<name>" — the kebab-cased tv() config name (e.g. the
notificationCard config produces notification-card), written on the
component's outermost rendered element only. This is the name the
component is registered under, not its filename — commandDialog →
command-dialog.data-part="<name>" — the kebab-cased tv() slot key, written on
every element that renders a slot (startContent → start-content).
Slot names are exactly the keys you already pass to @classes, so if you
can style a part with @classes={{hash startContent='...'}}, you can also
select it with [data-part="start-content"].The root element's part is usually base, but not always — it carries
whichever slot it actually renders. Table's root is its wrapper <div>, so
that element is data-part="wrapper"; SimpleTable used on its own roots on
the <table> element, so that one is data-part="table". ProgressBar's
outer <div> renders no slot at all, so it has data-component="progress-bar"
and no data-part. Check the component's page if you need the exact shape.
Both attributes can be overridden: a data-component or data-part you pass
to a component wins over the one it would render itself.
A nested component's root legitimately carries both its own
data-component and a data-part belonging to its parent. CloseButton
rendered inside Alert, for instance, is simultaneously
data-component="close-button" (its own root) and data-part="close-button"
(the part it fills in Alert's anatomy).
Two components can also render the same data-component value: TabNav is a
second renderer of the tabs config, alongside Tabs itself.
Scope a selector to one component's own parts with:
[data-component="modal"] [data-part="header"] {
/* ... */
}
[data-component="x"] [data-part="y"] is a plain CSS descendant combinator —
it matches any descendant, not just y parts that belong directly to
x. That's ambiguous whenever a nested component happens to have a part
with the same name. Alert renders a CloseButton, and CloseButton has
its own icon part (its SVG); Alert also has its own icon part (the
alert's leading icon). [data-component="alert"] [data-part="icon"] matches
both — the alert's own icon and the close button's icon glyph, because the
close button is a descendant of the alert's root.
What you usually mean is "the parts whose nearest [data-component] ancestor
is x". CSS has no "nearest enclosing" combinator, so in a stylesheet you
narrow the selector yourself, using child combinators down the path you want:
/* The alert's own icon. The close button's icon is nested one level
deeper, inside the close button, so it doesn't match. */
[data-component="alert"] > [data-part="inner"] > [data-part="icon"] {
/* ... */
}
In tests, use the ownParts(root, part) helper from frontile/test-support,
which returns only the parts belonging to root itself:
import { ownParts } from 'frontile/test-support';
// Only Alert's own `icon` part, not CloseButton's.
const icons = ownParts(alertRootElement, 'icon');
data-slotIf you're coming from shadcn/ui, Nuxt UI, or HeroUI, this convention will look
familiar but the attribute name differs — those use data-slot, Frontile uses
data-part. In Ember, "slot" already means a named block ({{yield to="title"}}),
which is a different thing from a styleable piece of the DOM.
@frontile/forms-legacy predates this convention and does not carry these
attributes.
To apply global styles to your Frontile components, it's recommended to create a separate file for your theme settings. You can create a file named app/theme.js to register your global custom styles, and then import it in app/app.js to ensure the styles are applied across your application.
Create a Theme File: Create a new file called app/theme.js and add your global customizations.
// app/theme.js
import { useStyles, registerCustomStyles, tv } from '@frontile/theme';
const components = useStyles();
registerCustomStyles({
// stuff ...
});
Import the Theme File: Import the theme file in app/app.js to ensure the global styles are applied.
// app/app.js
import Application from '@ember/application';
// stuff...
import './theme'; // Import your theme file here
export default class App extends Application {
// stuff...
}
loadInitializers(App, config.modulePrefix);
registerCustomStylesThe registerCustomStyles function allows you to override the default styles of Frontile components globally. This means that every instance of a specific component throughout your application will inherit the styles defined in registerCustomStyles.
To globally change the styles of the Drawer, Modal, and Button components:
import { useStyles, registerCustomStyles, tv } from '@frontile/theme';
const components = useStyles();
registerCustomStyles({
modal: tv({
extend: components.modal,
slots: {
header: 'font-header text-2xl pt-12 pl-6 pb-6',
body: 'pt-4 pb-4 pl-6 pr-12',
footer: 'bg-transparent py-4 px-12 pb-8'
},
variants: {
size: {
lg: 'max-w-[48rem]',
xl: 'max-w-[64rem]'
}
}
}),
drawer: tv({
extend: components.drawer,
slots: {
header: 'text-2xl px-4 py-6 bg-black text-white dark:bg-white dark:text-black rounded-none',
footer: 'px-4 py-6'
},
variants: {
size: {
lg: 'max-w-[48rem]',
xl: 'max-w-[64rem]'
}
}
}),
button: tv({
...components.button,
base: ['font-header text-xl'],
variants: {
...components.button.variants,
appearance: {
...components.button.variants.appearance,
default: 'shadow-elevation-2'
}
},
compoundVariants: [
...components.button.compoundVariants,
{
appearance: 'default',
intent: 'default',
class: 'bg-black text-white hover:bg-black/80 dark:bg-white dark:text-black dark:hover:bg-white/80'
}
]
})
});
In this example:
modal component is customized globally, including changes to the header, body, and footer slots, and additional size variants.drawer component is updated with new styles for the header and footer slots, as well as size variants.button component's base styles and compound variants are customized, adding new default appearances.Use global customization when you want consistency across your entire application for a particular component.
If you only need to customize the styles of a specific component instance, use arguments like class or classes, depending on the component. Frontile uses Tailwind Variants with tw-merge to merge Tailwind classes, so any class passed locally overwrites the default styles.
<Drawer
@isOpen={{true}}
@classes={{hash
header='bg-neutral-strong text-on-neutral-strong'
body='bg-neutral-subtle'
footer='bg-neutral-strong text-on-neutral-strong'
}}
>
<!-- Drawer content here -->
</Drawer>
In this example, the Drawer component is provided with specific styles for the header, body, and footer slots. The header and footer have a dark background (bg-neutral-strong) with automatically contrasting text (text-on-neutral-strong), while the body has a light background (bg-neutral-subtle). This approach allows for context-specific customizations without impacting other instances of the Drawer component.