Skip to content

Commit

Permalink
feat(ui): component theming (#5170)
Browse files Browse the repository at this point in the history
Co-authored-by: Caleb Pollman <[email protected]>
Co-authored-by: Jordan Van Ness <[email protected]>
  • Loading branch information
3 people authored Aug 8, 2024
1 parent e52db7b commit d73bd9c
Show file tree
Hide file tree
Showing 106 changed files with 3,426 additions and 365 deletions.
74 changes: 74 additions & 0 deletions .changeset/rad-cat-shred.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
"@aws-amplify/ui": minor
"@aws-amplify/ui-react": minor
---

feat(ui): experimental component theming

This feature lets you fully style and theme built-in components even if there is no design token available. For example, previously you could not add a box shadow or gradient background to the built-in Button component unless you wrote plain CSS. Now you can style every CSS property for all the built-in components with type-safety!

This also lets you define your own components and style them in the same type-safe way with zero runtime computation.

### defineComponentTheme()

```ts
import { defineComponentTheme } from '@aws-amplify/ui-react/server';

export const buttonTheme = defineComponentTheme({
// because 'button' is a built-in component, we get type-safety and hints
// based on the theme shape of our button
name: 'button',
theme: (tokens) => {
return {
textAlign: 'center',
padding: tokens.space.xl,
_modifiers: {
primary: {
backgroundColor: tokens.colors.primary[20],
},
},
};
},
});
```


### createTheme()

The theme object passed to `createTheme` now has an optional `components` array which is an array of component themes.

```ts
export const theme = createTheme({
name: 'my-theme',
components: [
buttonTheme,
customComponentTheme,
]
})
```

### React Server Component support for theming

You no longer need to use the `<ThemeProvider>` and rely on React context to theme Amplify UI (you still can though!). There is a new import path for RSC-compliant code: '@aws-amplify/ui-react/server' which you can use to import `createTheme` and `defineComponentTheme` as well as a new React Server Component: `<ThemeStyle />` which will inject the styles of your theme into the page.


```tsx
import { ThemeStyle, createTheme } from '@aws-amplify/ui-react/server';

const theme = createTheme({
//...
})

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div {...theme.containerProps({ colorMode: 'system' })}>
{children}
<ThemeStyle theme={theme} />
</div>
)
}
```
6 changes: 6 additions & 0 deletions examples/next-app-router/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/exhaustive-deps": "error" // override next eslint default
}
}
36 changes: 36 additions & 0 deletions examples/next-app-router/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
Empty file.
4 changes: 4 additions & 0 deletions examples/next-app-router/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = nextConfig;
22 changes: 22 additions & 0 deletions examples/next-app-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@aws-amplify/ui-next-app-example",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@aws-amplify/ui-react": "^6.1.0",
"next": "^14.1.1",
"react": "18.2.0",
"react-dom": "^18",
"react-icons": "^4.3.1"
},
"devDependencies": {
"@types/node": "^14.14.31",
"eslint-config-next": "^13.5.5"
}
}
5 changes: 5 additions & 0 deletions examples/next-app-router/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};
Binary file added examples/next-app-router/src/app/favicon.ico
Binary file not shown.
7 changes: 7 additions & 0 deletions examples/next-app-router/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@import '@aws-amplify/ui-react/styles/reset.css';
@import '@aws-amplify/ui-react/styles.css';

[data-amplify-theme] {
background-color: var(--amplify-colors-background-primary);
color: var(--amplify-colors-font-primary);
}
19 changes: 19 additions & 0 deletions examples/next-app-router/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import './globals.css';

export const metadata: Metadata = {
title: 'Amplify UI Next App Router Example',
description: 'Generated by create next app',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
18 changes: 18 additions & 0 deletions examples/next-app-router/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Avatar } from '@/components/Avatar';
import { theme } from '@/theme';
import Link from 'next/link';

export default function Home() {
return (
<main>
<Link href="/theme">Theme</Link>
<Link href="/theme-switcher">Theme Switcher</Link>
<Avatar />

<div {...theme.containerProps({ colorMode: 'dark' })}>
<h2>{`I'm dark`}</h2>
<Avatar />
</div>
</main>
);
}
25 changes: 25 additions & 0 deletions examples/next-app-router/src/app/theme-switcher/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import type { ColorMode } from '@aws-amplify/ui-react';
import { ThemeStyle } from '@aws-amplify/ui-react/server';
import { theme } from '@/theme';
import { Header } from '@/components/Header';
import ThemeToggle from '@/components/ThemeToggle';
import { cookies } from 'next/headers';

export default function Layout({ children }: { children: React.ReactNode }) {
const cookieStore = cookies();
const colorMode = (cookieStore.get('colorMode')?.value ??
'dark') as ColorMode;

return (
<div {...theme.containerProps({ colorMode })}>
{/* Header */}
<Header>
Amplify UI RSC
<ThemeToggle initialValue={colorMode} />
</Header>
{children}
<ThemeStyle theme={theme} />
</div>
);
}
31 changes: 31 additions & 0 deletions examples/next-app-router/src/app/theme-switcher/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ThemeStyle } from '@aws-amplify/ui-react/server';
import { Avatar } from '@/components/Avatar';
import { theme } from '@/theme';

export default function ThemeSwitcherPage() {
return (
<div
className="flex w-full flex-row"
style={{
backgroundColor: `var(--breakpoint-large, #f90)`,
}}
>
<Avatar size="small" />
<Avatar />
<Avatar size="large" />
<div {...theme.containerProps({ colorMode: 'dark' })}>
<div
className="flex w-full flex-row"
style={{
backgroundColor: `${theme.tokens.colors.background.primary}`,
}}
>
<Avatar size="small" />
<Avatar />
<Avatar size="large" />
</div>
</div>
<ThemeStyle theme={theme} />
</div>
);
}
30 changes: 30 additions & 0 deletions examples/next-app-router/src/app/theme/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
ThemeStyle,
createComponentClasses,
} from '@aws-amplify/ui-react/server';
import { theme } from '@/theme';

const headingClasses = createComponentClasses({ name: 'heading' });

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex w-full flex-row">
<div className="flex-1 p-2">
<h2 className={headingClasses({ _modifiers: ['2'] })}>Custom theme</h2>

<ThemeStyle theme={theme} />
<section {...theme.containerProps({ colorMode: 'dark' })}>
{children}
</section>
</div>
<div className="flex-1 p-2">
<h2 className={headingClasses({ _modifiers: ['2'] })}>Default theme</h2>
{children}
</div>
</div>
);
}
75 changes: 75 additions & 0 deletions examples/next-app-router/src/app/theme/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client';
import { Avatar } from '@/components/Avatar';
import { MyClientComponent } from '@/components/ClientComponent';
import { MyServerComponent } from '@/components/ServerComponent';
import { theme } from '@/theme';
import {
Alert,
Badge,
BadgeProps,
Button,
ButtonProps,
Flex,
Heading,
Text,
} from '@aws-amplify/ui-react';

const colorThemes: BadgeProps['variation'][] = [
undefined,
'success',
'info',
'warning',
'error',
];

export default function ThemePage() {
return (
<Flex direction="column">
<Heading level={3}>Badges</Heading>
<Flex direction="row">
{colorThemes.map((colorTheme, i) => (
<Badge key={`${i}-${colorTheme}`} variation={colorTheme}>
{colorTheme || 'default'}
</Badge>
))}
</Flex>
<Heading level={3}>Buttons</Heading>
<Flex direction="row">
{colorThemes.map((colorTheme, i) => (
<Button key={`${i}-${colorTheme}`} colorTheme={colorTheme}>
{colorTheme || 'default'}
</Button>
))}
</Flex>
<Flex direction="row">
{colorThemes.map((colorTheme, i) => (
<Button
key={`${i}-${colorTheme}`}
variation="link"
colorTheme={colorTheme}
>
{colorTheme || 'default'}
</Button>
))}
</Flex>
<Flex direction="row">
{colorThemes.map((colorTheme, i) => (
<Button
key={`${i}-${colorTheme}`}
variation="primary"
colorTheme={colorTheme}
>
{colorTheme || 'default'}
</Button>
))}
</Flex>
<Avatar isDisabled />
<MyClientComponent />
<MyServerComponent />
<Text color={theme.tokens.colors.font.success}>Success!</Text>
<Alert heading="Hello" />
<Alert heading="Hello success" variation="success" />
<Alert heading="Hello" variation="info" />
</Flex>
);
}
Loading

0 comments on commit d73bd9c

Please sign in to comment.