Transcript
[00:00] Astryx design system relies on Facebook’s, I’m sorry, Meta’s, StyleX library for styling. It all comes pre-compiled, so you don’t actually have to do anything. But once you go look at StyleX and say, cool, I want to start, you know, doing some custom styles and all sorts of stuff and use this StyleX thing, you’re going to run into a bunch of problems. So that’s what we’re going to cover here. You know, it’s a little work you’ve got to put in up front, but it’s not that bad.
[00:30] What we’re going to do to avoid a bunch of errors is we are going to install the StyleX JS Babel plugin and the PostCSS plugin, as well as auto-prefixer. Now, once that’s set up, we are going to jump over to our app. In our root directory, we’re going to create a new file, babelconfig.js. We’re also going to create a new file, postCSSConfig.js.
[00:56] For a babelconfig, I’m going to drop this in here. Don’t worry about it. I’m going to have links to all this code for you. There’s nothing super interesting about this. This is just standard babelconfig stuff. Yeah, I don’t really see anything special in here. You just need to have this in place. I should point out this is very specific to Next.js 16. I think you run into the same types of problems with Vite and other frameworks. So if you’re working with one of those, you’ll just have to figure it out. But for Next.js 16, this is what you want to do.
[01:30] And then on our postCSS, very similar. We’re going to bring in that babelconfig and we’re running this plugin, this style XJS postCSS plugin, and just making sure it’s got access to all the files we’re using. And that’s pretty much it. Now we’re looking good. And then we’re going to come over here to our global CSS. And right down here, after those imports, we’re going to drop in the style X import. And that’s all we need to do there. For good measure, I’m going to remove my local next cache. And I’m going to restart my server. And everything seems to be working fine.
[02:11] Here in our source app, I’m going to create a new TypeScript file called styles TS. Into that, I’m going to import style X. And this is the way we create custom styles in style X. So I’m going to be exporting something called styles. And then it’s style X.create. And then we basically just drop in JS, JavaScriptified CSS objects. So for example, we’re going to have a boards column. And you can see, you know, overflow X becomes capital X. You know, all the, what do they call that? Pascal casing. I can’t remember. Overflow Y height, all of that.
[02:50] You do get access to any of the variables available in the imports we already have in our global CSS. So I’m going to drop a bunch of these. So I’m going to drop a bunch of these in here. We’ll take a look at a couple of them, but there’s nothing really super interesting here. Oh, this column width, I forgot about that. I’m going to drop this column width right here. And that one’s kind of tricky because previously I tried this where I was importing it from another version of this application. And that didn’t work. Just so you know, like all of this has to be pre-compiled. So you can’t do a lot of dynamic stuff here.
[03:30] We are going to talk about dynamic styling, actually. But in this case, I just want to point out that column width is here in this file. And it kind of needs to be that way because this is going to get compiled before runtime or at runtime. Either way, you can’t do a lot of like importing from other files and things of that nature here. So column width is right here in the file. The only other thing I’d point out is I do have some like media prefers, which is going to work well with our preferences that we set up. So we’ve got that use preferences hook that we created earlier. So system preferences versus light versus dark. I do have some elements in here to handle that. You can see me jumping into tokens that are available to us from the library. So there’s our style TS. Actually, it’s going to be styles TS.
[04:28] We’re going to jump over to our Kanban. We’re going to be bringing in VStack. And we’re going to bring in our styles. Coming down here to our content section, I’m going to update this to a padding of zero. I just know that it’s going to end up looking better. This will also be a padding of zero. And then here, instead of flex shrink, we’re going to say styles dot column, column shell. So I’m just pulling the style directly from the styles typescript file that we just created. In this section, I’m more or less just wrapping in a VStack. Format that. You can see here, card padding, I’m actually using X style, which we will talk more about. We’ve got our standard heading and all that good stuff.
[05:19] If we jump over to our application, it’s kind of starting to take shape. Now you can see I got a little hover effect here. And everything’s kind of starting to come together. So this X style thing, every component in Astryx accepts an X style property. And what that does is it allows you to customize the component without breaking the encapsulation they already created. So it allows us to like bleed into their system without jacking it all up. I don’t know. It’s pretty cool. And we will be using that further as we move along. But that is the process by which you can integrate style X into your Next.js 16 application.
Astryx relies on Meta’s StyleX (@stylexjs/stylex) for styling. StyleX compiles styles down to atomic, collision-free CSS classes at build time. In this lesson we configure the compiler once, write the board’s centralized stylesheet (src/app/styles.ts), and apply it to the shell and placeholder columns via the xstyle prop.
Files this lesson: babel.config.js (new), postcss.config.js (new), src/app/globals.css (extended), src/app/styles.ts (new), src/app/components/KanbanBoard.tsx (rewritten). page.tsx stays untouched.
Prerequisite: Configure the StyleX Compiler (Next.js 16 + Turbopack)
Astryx ships pre-compiled, so consuming its components needs no StyleX compiler setup (see lesson 1). But stylex.create is compile-time-only: when a call reaches the runtime un-compiled, @stylexjs/stylex throws:
Unexpected 'stylex.create' call at runtime. Styles must be compiled by '@stylexjs/babel-plugin'.Configure the compiler once per project before writing the styles below. The same setup covers the custom StyleX you’ll write in lessons 8 and 12, so you only pay this cost once.
1. Install the Compiler Plugins
npm install -D @stylexjs/babel-plugin @stylexjs/postcss-plugin autoprefixerKeep
@stylexjs/babel-pluginand@stylexjs/postcss-pluginin sync with the@stylexjs/stylexversion installed in lesson 1 (check withnpm list @stylexjs/stylex).
2. Babel Configuration
Create babel.config.js in the project root. The StyleX Babel plugin transforms every stylex.create call into compiled, atomic CSS references at build time:
const path = require('path');const dev = process.env.NODE_ENV !== 'production';
module.exports = { presets: ['next/babel'], plugins: [ [ '@stylexjs/babel-plugin', { dev, runtimeInjection: false, enableInlinedConditionalMerge: true, treeshakeCompensation: true, aliases: { '@/*': [path.join(__dirname, 'src/*')] }, unstable_moduleResolution: { type: 'commonJS' }, }, ], ],};The
aliasesmapping assumes the--src-dirlayout from lesson 1 (@/*→./src/*). The board’s own imports are all relative, so this mainly future-proofs alias usage.
3. PostCSS Configuration
Create postcss.config.js. The PostCSS plugin generates the compiled CSS rules and reuses the same Babel plugin config so both halves of the pipeline agree on the transformation:
const babelConfig = require('./babel.config');
module.exports = { plugins: { '@stylexjs/postcss-plugin': { include: [ 'src/**/*.{js,jsx,ts,tsx}', 'app/**/*.{js,jsx,ts,tsx}', 'components/**/*.{js,jsx,ts,tsx}', ], babelConfig: { babelrc: false, parserOpts: { plugins: ['typescript', 'jsx'] }, plugins: babelConfig.plugins, }, useCSSLayers: true, }, autoprefixer: {}, },};The include globs tell the plugin which files may contain StyleX styles — make sure they cover src/ (and therefore src/app/components/ and src/app/hooks/) when using the src directory layout.
4. Add the @stylex Directive
The PostCSS plugin injects the compiled atomic CSS where the @stylex directive appears. Append it to src/app/globals.css, after the Astryx imports:
@import '@astryxdesign/core/reset.css';@import '@astryxdesign/core/astryx.css';@import '@astryxdesign/theme-neutral/theme.css';
@stylex;5. Restart the Dev Server
Clear the Next.js cache and restart. Assuming you’re on the Next.js 16 template that create-next-app@latest installs today, no next.config changes are needed — since Next.js 16.0.3 this setup runs under Turbopack, the default bundler:
rm -rf .nextnpm run devTurbopack giving you trouble?
next dev --webpackopts out on Next.js 16, but the setup above is the tested Turbopack path.
The Board’s Centralized Stylesheet: styles.ts
Create src/app/styles.ts. This file holds every custom StyleX rule the board needs — layout, elevation, drag feedback. One stylesheet, imported by every component:
import * as stylex from '@stylexjs/stylex';
// StyleX inlines every value inside stylex.create at build time, so the// compiler needs the width defined here rather than imported from './data'// (see the note below).const COLUMN_WIDTH = 300;
export const styles = stylex.create({ boardColumns: { overflowX: 'auto', overflowY: 'hidden', height: '100%', padding: 'var(--spacing-4)', }, columnShell: { flexShrink: 0, flexBasis: COLUMN_WIDTH, height: '100%', }, card: { cursor: 'grab', userSelect: 'none', touchAction: 'none', transition: 'box-shadow 120ms ease', ':hover': { boxShadow: 'var(--shadow-med)', }, // Black shadows vanish on dark surfaces, so strengthen the hover state // with a subtle light ring and a deeper drop shadow in dark mode. '@media (prefers-color-scheme: dark)': { ':hover': { boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.14), 0 8px 24px rgba(0, 0, 0, 0.55)', }, }, }, floating: { position: 'fixed', insetBlockStart: 0, insetInlineStart: 0, pointerEvents: 'none', cursor: 'grabbing', boxShadow: 'var(--shadow-high)', zIndex: 1000, '@media (prefers-color-scheme: dark)': { boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.16), 0 12px 32px rgba(0, 0, 0, 0.6)', }, }, floatingAt: (x: number, y: number, width: number) => ({ width, transform: `translate(${x}px, ${y}px)`, }), ghost: (height: number) => ({ height, borderRadius: 'var(--radius-container)', backgroundColor: 'var(--color-background-muted)', '@media (prefers-color-scheme: dark)': { outline: '1px dashed rgba(148, 163, 184, 0.5)', }, }), toolbarDivider: { height: 'auto', marginBlock: 'var(--spacing-1)', alignSelf: 'stretch', }, columnEmptyState: { paddingBlock: 'var(--spacing-10)', },});What’s in here
boardColumns— the horizontal strip: it scrolls on the x-axis when columns overflow, never on y.columnShell— a fixed-width, full-height column.flexBasis: COLUMN_WIDTHsizes it;flexShrink: 0keeps it from squishing.card— the interactive card base: grab cursor, no text selection, and a hover elevation that transitions in over 120ms. The@media (prefers-color-scheme: dark)conditional swaps the near-invisible black shadow for a light ring + deep shadow — StyleX nests media queries and pseudo-classes exactly like CSS.floating,floatingAt,ghost— the drag feedback styles.floatingpins a clone to the viewport with high elevation;floatingAt(x, y, width)is a dynamic function that positions it viatransform;ghost(height)renders the drop-slot placeholder. These get used in lessons 11–12 — defined now, sostyles.tsis the single source for the whole app.toolbarDivider,columnEmptyState— vertical divider sizing for the toolbar (lesson 10) and vertical padding for the compact empty state (lesson 9).
Why the local
COLUMN_WIDTHinstead ofimport { COLUMN_WIDTH } from './data'? StyleX evaluates every value insidestylex.createat build time. The PostCSS plugin processesstyles.tson its own, and its CommonJS resolver can’t load./data— a.tsmodule that also imports icons and types — so the import fails with “Could not resolve the path to the imported file.” Values used insidestylex.createmust be defined in the same file — or in a.stylex.tsfile, StyleX’s dedicated constant/theme module format, which the compiler can resolve. Whendata.tsarrives in lesson 6 it keeps its ownCOLUMN_WIDTHexport for reference; it just can’t feed a StyleX file.
The xstyle Prop
Every core Astryx component accepts an xstyle prop. This applies custom StyleX rules directly onto Astryx elements without breaking their design-system encapsulations. You can pass a single style object, or an array of them:
<Card xstyle={[styles.card, styles.floatingAt(x, y, width)]}> {children}</Card>Applying the Styles: KanbanBoard.tsx
Rewrite src/app/components/KanbanBoard.tsx to use the stylesheet — the column strip gets boardColumns, each column gets columnShell, and each placeholder task card gets card (so it lifts on hover):
'use client';
import { Section } from '@astryxdesign/core/Section';import { Layout, LayoutHeader, LayoutContent, HStack, VStack,} from '@astryxdesign/core/Layout';import { Heading, Text } from '@astryxdesign/core/Text';import { Card } from '@astryxdesign/core/Card';import { Button } from '@astryxdesign/core/Button';import { useTranslator } from '@astryxdesign/core/i18n';import { usePreferences } from '../providers';import { styles } from '../styles';
export default function KanbanBoard() { const t = useTranslator(); const { locale, setLocale } = usePreferences();
return ( <Section height="100dvh"> <Layout height="fill" header={ <LayoutHeader hasDivider padding={4}> <HStack hAlign="between" vAlign="center"> <Heading level={3}>{t('@app.board.title')}</Heading> <HStack gap={2} vAlign="center"> {/* Tabular numbers keep digits from jittering as counts change */} <Text type="supporting" color="secondary" hasTabularNumbers> 4 {t('@app.tasks')} </Text> <Button label={t('@app.switchLanguage')} variant="secondary" onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')} /> </HStack> </HStack> </LayoutHeader> } content={ <LayoutContent padding={4}> <LayoutContent padding={0}> <HStack gap={4}> <HStack gap={4} xstyle={styles.boardColumns}> {[0, 1, 2, 3].map(i => ( <Card key={i} variant="muted" padding={3} width={300} style={{ flexShrink: 0 }} padding={0} xstyle={styles.columnShell} > <Heading level={4}>Column {i + 1}</Heading> <Text type="supporting" color="secondary"> Placeholder — data arrives in lesson 6. </Text> <VStack gap={3} padding={3}> <Heading level={4}>Column {i + 1}</Heading> <Card padding={3} xstyle={styles.card}> <Heading level={5}>Placeholder task</Heading> <Text type="supporting" color="secondary" maxLines={2}> A placeholder card inside the styled column shell. </Text> </Card> </VStack> </Card> ))} </HStack> </LayoutContent> } /> </Section> );}Note LayoutContent padding={0} — the column strip owns its own padding via styles.boardColumns, so the content body adds none.
See It in Action
Run npm run dev (after clearing .next once). Hover any placeholder task card — it lifts with the --shadow-med elevation. In dark mode (mode="system" follows your OS), the hover picks up the light ring instead. The columns keep their fixed 300px width and the strip scrolls horizontally if the window is too narrow.
The floating and ghost rules are compiled but not yet visible — they come alive with drag-and-drop in lessons 11–12.
What You Built
- The one-time StyleX compiler setup (Babel + PostCSS +
@stylexdirective). src/app/styles.ts— the single stylesheet for the whole board, including dark-mode-aware hover and the drag feedback functions.- The
xstyleprop wired into the real board: strip, column shells, and hoverable cards.
In the next lesson, we will break down the theme system and design tokens — and add a light/dark toggle to the board.