@hxnova/themes
v1.0.0
Published
This package provides a Material Design 3 based theming system for styling Nova applications.
Readme
@hxnova/themes
This package provides a Material Design 3 based theming system for styling Nova applications.
Installation
# With npm
npm install @hxnova/themes@beta
# With pnpm
pnpm add @hxnova/themes@beta
# With yarn
yarn add @hxnova/themes@betaUsage
Configuring the theme
Import the required stylesheets and set up the theme provider in your application entry point:
import React from 'react';
import '@hxnova/themes/styles.css';
import '@hxnova/icons/icons.css';
import { CssBaseline } from '@hxnova/react-components/CssBaseline';
import { ThemeProvider } from '@hxnova/react-components/styles';
import novaTheme from '@hxnova/themes';
<ThemeProvider theme={novaTheme}>
<CssBaseline />
{/* Your Application Contents Here */}
</ThemeProvider>Fonts And additional CSS style
The styles.css file includes:
- Hexagon Akkurat font styles for Typography components
- Viewport related tokens (radius, spacing, height, icon size, outline, and component dimensions)
Note: Make sure your project is licensed to use the Hexagon Akkurat font.
// index.css
@import '@hxnova/themes/styles.css';
// index.js
import '@hxnova/themes/styles.css';Support Light & Dark Theme
Use the useColorScheme hook to toggle between light and dark themes:
import React from 'react';
import { useColorScheme } from '@hxnova/react-components/styles';
import { IconButton } from '@hxnova/react-components/IconButton';
function ColorSchemeToggleButton() {
const { mode, setMode } = useColorScheme();
const toggleColorScheme = () => {
setMode(mode === 'dark' ? 'light' : 'dark');
};
return (
<IconButton onClick={toggleColorScheme}>
{mode === 'light' ? '🌙' : '🔆'}
</IconButton>
);
}Use the theme
Using styled components
import { styled } from '@hxnova/react-components/styled';
const StyledComponent = styled('div')(({ theme }) => ({
backgroundColor: theme.vars.palette.primary,
color: theme.vars.palette.onPrimary,
borderRadius: theme.vars.sys.size.radius.md.medium,
boxShadow: theme.shadows[2],
padding: 2,
[theme.breakpoints.up('md')]: {
padding: 3,
},
}));Using with sx prop
<Box sx={{ border: 1 }} />
// equivalent to border: '1px solid black'
<Box sx={{ bgcolor: 'primary' }} />
// equivalent to backgroundColor: theme => theme.vars.palette.primary
<Box sx={{ margin: 2 }} />
// equivalent to margin: calc(2 * var(--nova-spacing, 4px))Using CSS Variables directly
All design tokens are available as CSS custom properties:
.my-component {
/* Color tokens */
background-color: var(--nova-palette-primary);
color: var(--nova-palette-onPrimary);
border-color: var(--nova-palette-outline);
/* Size tokens */
padding: var(--space-between-horizontal-2xs);
border-radius: var(--radius-md);
}Using Primitive Colors (Not Recommended)
You can access primitive colors if needed, but it's not recommended as they don't follow the Material Design 3 semantic color system:
import { primitives } from '@hxnova/themes';
// Not recommended - bypasses semantic color roles
const myComponent = {
backgroundColor: primitives.colors.nova.brandBlue[500],
color: primitives.nova.colors.nova.neutral[100],
};Note: Always prefer using semantic color tokens (primary, surface, outline, etc.) over primitive colors for better theme consistency and accessibility.
Using Breakpoints with useMediaQuery
Use the useMediaQuery hook with Nova's breakpoint system for responsive design:
Available Breakpoints
xs: 0px (Mobile)sm: 600px (Tablet Portrait)md: 840px (Tablet Landscape)lg: 1200px (Laptop Display)xl: 1600px (Desktop Display)
Example Usage
import { useMediaQuery } from '@hxnova/react-components';
import { NovaTheme } from '@hxnova/themes';
function ResponsiveComponent() {
const isMobile = useMediaQuery(NovaTheme.breakpoints.down('sm'));
const isTablet = useMediaQuery(NovaTheme.breakpoints.between('sm', 'md'));
const isDesktop = useMediaQuery(NovaTheme.breakpoints.up('lg'));
return (
<div>
{isMobile && <MobileLayout />}
{isTablet && <TabletLayout />}
{isDesktop && <DesktopLayout />}
</div>
);
}Next.js Integration
Configuring the theme
Next.js requires additional configuration for server-side rendering (SSR) support:
1. Install required dependencies:
npm install @emotion/cache @emotion/react @emotion/styled2. Import stylesheets in your global CSS file (app/globals.css):
@import '@hxnova/themes/styles.css';
@import '@hxnova/icons/icons.css';
/* Your other global styles */3. Add an AppCacheProvider to make emotion-styled Nova components work with the Next.js project:
'use client';
import * as React from 'react';
import createCache, { EmotionCache } from '@emotion/cache';
import { CacheProvider as DefaultCacheProvider } from '@emotion/react';
import { useServerInsertedHTML } from 'next/navigation';
export type AppCacheProviderProps = {
/**
* By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"'.
*/
CacheProvider?: React.ElementType<{ value: EmotionCache }>;
children: React.ReactNode;
};
/**
* Emotion works OK without this provider but it's recommended to use this provider to improve performance.
* Without it, Emotion will generate a new <style> tag during SSR for every component.
* See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 for why it's a problem.
*/
export default function AppCacheProvider(props: AppCacheProviderProps) {
const { CacheProvider = DefaultCacheProvider, children } = props;
const [{ cache, flush }] = React.useState(() => {
const cache = createCache({ key: 'nova' });
cache.compat = true;
const prevInsert = cache.insert;
let inserted: { name: string; isGlobal: boolean }[] = [];
// Override the insert method to support streaming SSR with flush().
cache.insert = (...args) => {
const [selector, serialized] = args;
if (cache.inserted[serialized.name] === undefined) {
inserted.push({
name: serialized.name,
isGlobal: !selector,
});
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const inserted = flush();
if (inserted.length === 0) {
return null;
}
let styles = '';
let dataEmotionAttribute = cache.key;
const globals: {
name: string;
style: string;
}[] = [];
inserted.forEach(({ name, isGlobal }) => {
const style = cache.inserted[name];
if (typeof style === 'string') {
if (isGlobal) {
globals.push({ name, style });
} else {
styles += style;
dataEmotionAttribute += ` ${name}`;
}
}
});
return (
<React.Fragment>
{globals.map(({ name, style }) => (
<style
key={name}
data-emotion={`${cache.key}-global ${name}`}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style }}
/>
))}
{styles && (
<style
data-emotion={dataEmotionAttribute}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: styles }}
/>
)}
</React.Fragment>
);
});
return <CacheProvider value={cache}>{children}</CacheProvider>;
}4. Configure the layout (app/layout.tsx):
'use client';
import React from 'react';
import { CssBaseline } from '@hxnova/react-components/CssBaseline';
import { ThemeProvider } from '@hxnova/react-components/styles';
import novaTheme from '@hxnova/themes';
import './globals.css';
import AppCacheProvider from '@/components/AppCacheProvider';
import InitColorSchemeScript from '@hxnova/react-components/InitColorSchemeScript';
export default async function RootLayout(props: { children: React.ReactNode }) {
return (
<AppCacheProvider>
<ThemeProvider theme={novaTheme}>
<html lang="en">
<CssBaseline />
<body>
<InitColorSchemeScript attribute="class" />
{props.children}
</body>
</html>
</ThemeProvider>
</AppCacheProvider>
);
}
Support Light & Dark Theme
For Next.js, theme switching requires special handling due to SSR. Use the useColorScheme hook in client components:
'use client';
import React from 'react';
import { useColorScheme } from '@hxnova/react-components/styles';
import { IconButton } from '@hxnova/react-components/IconButton';
function ColorSchemeToggleButton() {
const { mode, setMode } = useColorScheme();
const toggleColorScheme = () => {
setMode(mode === 'dark' ? 'light' : 'dark');
};
return (
<IconButton onClick={toggleColorScheme}>
{mode === 'light' ? '🌙' : '🔆'}
</IconButton>
);
}