├── .prettierignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── workflows │ └── ci.yml ├── .husky └── commit-msg ├── .storybook ├── preview.css ├── preview.ts └── main.ts ├── .vscode └── settings.json ├── tsconfig.build.json ├── docs ├── assets │ ├── chessboard.png │ ├── wood-pattern.png │ └── 3d-pieces │ │ ├── bB.webp │ │ ├── bK.webp │ │ ├── bN.webp │ │ ├── bP.webp │ │ ├── bQ.webp │ │ ├── bR.webp │ │ ├── wB.webp │ │ ├── wK.webp │ │ ├── wN.webp │ │ ├── wP.webp │ │ ├── wQ.webp │ │ └── wR.webp ├── stockfish │ ├── stockfish.wasm │ └── engine.ts ├── stories │ ├── options │ │ ├── ChessboardColumns.stories.tsx │ │ ├── DarkSquareStyle.stories.tsx │ │ ├── LightSquareStyle.stories.tsx │ │ ├── DarkSquareNotationStyle.stories.tsx │ │ ├── AlphaNotationStyle.stories.tsx │ │ ├── LightSquareNotationStyle.stories.tsx │ │ ├── NumericNotationStyle.stories.tsx │ │ ├── SquareStyle.stories.tsx │ │ ├── BoardStyle.stories.tsx │ │ ├── DropSquareStyle.stories.tsx │ │ ├── SquareRenderer.stories.tsx │ │ ├── CanDragPiece.stories.tsx │ │ ├── Pieces.stories.tsx │ │ ├── ShowNotation.stories.tsx │ │ ├── AllowDragging.stories.tsx │ │ ├── AllowDragOffBoard.stories.tsx │ │ ├── AllowDrawingArrows.stories.tsx │ │ ├── AllowAutoScroll.stories.tsx │ │ ├── ClearArrowsOnClick.stories.tsx │ │ ├── DragActivationDistance.stories.tsx │ │ ├── OnMouseOutSquare.stories.tsx │ │ ├── OnMouseOverSquare.stories.tsx │ │ ├── OnSquareClick.stories.tsx │ │ ├── BoardOrientation.stories.tsx │ │ ├── OnPieceDrag.stories.tsx │ │ ├── OnPieceClick.stories.tsx │ │ ├── ChessboardRows.stories.tsx │ │ ├── OnSquareRightClick.stories.tsx │ │ ├── SquareStyles.stories.tsx │ │ ├── OnPieceDrop.stories.tsx │ │ ├── OnSquareMouseUp.stories.tsx │ │ ├── DraggingPieceStyle.stories.tsx │ │ ├── DraggingPieceGhostStyle.stories.tsx │ │ ├── OnSquareMouseDown.stories.tsx │ │ ├── Arrows.stories.tsx │ │ ├── Position.stories.tsx │ │ ├── ShowAnimations.stories.tsx │ │ ├── ClearArrowsOnPositionChange.stories.tsx │ │ ├── AnimationDurationInMs.stories.tsx │ │ ├── OnArrowsChange.stories.tsx │ │ └── ArrowOptions.stories.tsx │ ├── basic-examples │ │ ├── Default.stories.tsx │ │ ├── PlayVsRandom.stories.tsx │ │ ├── SparePieces.stories.tsx │ │ ├── ClickToMove.stories.tsx │ │ └── ClickOrDragToMove.stories.tsx │ └── advanced-examples │ │ ├── Multiplayer.stories.tsx │ │ ├── 3DBoard.stories.tsx │ │ ├── MiniPuzzles.stories.tsx │ │ ├── AnalysisBoard.stories.tsx │ │ ├── Premoves.stories.tsx │ │ ├── PiecePromotion.stories.tsx │ │ └── FourPlayerChess.stories.tsx ├── components │ ├── HintMessage.tsx │ ├── WarningMessage.tsx │ └── DocNavigation.tsx ├── A_GetStarted.mdx ├── B_BasicExamples.mdx ├── F_Contributing.mdx └── C_AdvancedExamples.mdx ├── .npmignore ├── commitlint.config.mjs ├── src ├── index.ts ├── Droppable.tsx ├── SparePiece.tsx ├── Chessboard.tsx ├── RightClickCancelSensor.tsx ├── Draggable.tsx ├── types.ts ├── modifiers.ts ├── Board.tsx ├── defaults.ts ├── Piece.tsx └── Arrows.tsx ├── release.config.mjs ├── .gitignore ├── rollup.config.js ├── eslint.config.mjs ├── prettier.config.mjs ├── LICENSE ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | **/stockfish.wasm.js -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Clariity] 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint ${1} 2 | -------------------------------------------------------------------------------- /.storybook/preview.css: -------------------------------------------------------------------------------- 1 | .docs-story > div { 2 | overflow: hidden !important; 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["docs"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/assets/chessboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/chessboard.png -------------------------------------------------------------------------------- /docs/assets/wood-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/wood-pattern.png -------------------------------------------------------------------------------- /docs/assets/3d-pieces/bB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/bB.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/bK.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/bK.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/bN.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/bN.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/bP.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/bP.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/bQ.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/bQ.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/bR.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/bR.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/wB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/wB.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/wK.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/wK.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/wN.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/wN.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/wP.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/wP.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/wQ.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/wQ.webp -------------------------------------------------------------------------------- /docs/assets/3d-pieces/wR.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/assets/3d-pieces/wR.webp -------------------------------------------------------------------------------- /docs/stockfish/stockfish.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clariity/react-chessboard/HEAD/docs/stockfish/stockfish.wasm -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .storybook 2 | media 3 | node_modules 4 | src 5 | stories 6 | storybook-static 7 | .gitignore 8 | package-lock.json 9 | rollup.config.js 10 | vercel.json -------------------------------------------------------------------------------- /commitlint.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [0, 'always', 100], // [enabled, condition, value] 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Chessboard'; 2 | export * from './ChessboardProvider'; 3 | export * from './defaults'; 4 | export * from './pieces'; 5 | export * from './SparePiece'; 6 | export * from './types'; 7 | export * from './utils'; 8 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import "./preview.css"; 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | deepControls: { enabled: true }, 13 | }, 14 | }; 15 | 16 | export default preview; 17 | -------------------------------------------------------------------------------- /src/Droppable.tsx: -------------------------------------------------------------------------------- 1 | import { useDroppable } from '@dnd-kit/core'; 2 | 3 | type DroppableProps = { 4 | children: (props: { isOver: boolean }) => React.ReactNode; 5 | squareId: string; 6 | }; 7 | 8 | export function Droppable({ children, squareId }: DroppableProps) { 9 | const { isOver, setNodeRef } = useDroppable({ 10 | id: squareId, 11 | }); 12 | 13 | return
{children({ isOver })}
; 14 | } 15 | -------------------------------------------------------------------------------- /release.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | branches: ['main', { name: 'beta', prerelease: true }], 3 | plugins: [ 4 | ['@semantic-release/commit-analyzer', { preset: 'conventionalcommits' }], 5 | [ 6 | '@semantic-release/release-notes-generator', 7 | { preset: 'conventionalcommits' }, 8 | ], 9 | '@semantic-release/npm', 10 | '@semantic-release/github', 11 | ], 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /src/SparePiece.tsx: -------------------------------------------------------------------------------- 1 | import { Draggable } from './Draggable'; 2 | import { Piece } from './Piece'; 3 | 4 | type SparePieceProps = { 5 | pieceType: string; 6 | }; 7 | 8 | export function SparePiece({ pieceType }: SparePieceProps) { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | storybook-static 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | *storybook.log -------------------------------------------------------------------------------- /src/Chessboard.tsx: -------------------------------------------------------------------------------- 1 | import { Board } from './Board'; 2 | import { 3 | ChessboardOptions, 4 | ChessboardProvider, 5 | useChessboardContext, 6 | } from './ChessboardProvider'; 7 | 8 | type ChessboardProps = { 9 | options?: ChessboardOptions; 10 | }; 11 | 12 | export function Chessboard({ options }: ChessboardProps) { 13 | const { isWrapped } = useChessboardContext() ?? { isWrapped: false }; 14 | 15 | if (isWrapped) { 16 | return ; 17 | } 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | 5 | import pkg from './package.json' assert { type: 'json' }; 6 | 7 | export default { 8 | input: 'src/index.ts', 9 | output: [ 10 | { 11 | file: pkg.main, 12 | format: 'cjs', 13 | exports: 'auto', 14 | }, 15 | { 16 | file: pkg.module, 17 | format: 'esm', 18 | }, 19 | ], 20 | plugins: [ 21 | resolve(), 22 | commonjs(), 23 | typescript({ 24 | clean: true, 25 | tsconfig: 'tsconfig.build.json', 26 | }), 27 | ], 28 | external: ['react', 'react-dom', 'react/jsx-runtime'], 29 | }; 30 | -------------------------------------------------------------------------------- /docs/stories/options/ChessboardColumns.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/ChessboardColumns', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const ChessboardColumns: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | chessboardColumns: 16, 19 | id: 'chessboard-columns', 20 | }; 21 | 22 | // render 23 | return ; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /docs/stories/basic-examples/Default.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Chessboard } from '../../../src'; 4 | 5 | const meta: Meta = { 6 | title: 'stories/Default', 7 | component: Chessboard, 8 | parameters: { 9 | layout: 'fullscreen', 10 | }, 11 | decorators: [ 12 | (Story) => ( 13 |
20 | 21 |
22 | ), 23 | ], 24 | } satisfies Meta; 25 | 26 | export default meta; 27 | 28 | type Story = StoryObj; 29 | 30 | export const Default: Story = { 31 | render: () => , 32 | }; 33 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import { defineConfig, globalIgnores } from 'eslint/config'; 3 | import prettier from 'eslint-config-prettier/flat'; 4 | import react from 'eslint-plugin-react'; 5 | import globals from 'globals'; 6 | import typescript from 'typescript-eslint'; 7 | 8 | export default defineConfig([ 9 | globalIgnores(['**/stockfish.wasm.js']), 10 | eslint.configs.recommended, 11 | typescript.configs.recommended, 12 | react.configs.flat.recommended, 13 | react.configs.flat['jsx-runtime'], 14 | prettier, 15 | { 16 | files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'], 17 | languageOptions: { 18 | globals: globals.browser, 19 | }, 20 | settings: { 21 | react: { 22 | version: 'detect', 23 | }, 24 | }, 25 | }, 26 | ]); 27 | -------------------------------------------------------------------------------- /docs/stories/options/DarkSquareStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/DarkSquareStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const DarkSquareStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | darkSquareStyle: { 19 | backgroundColor: 'cyan', 20 | }, 21 | id: 'dark-square-style', 22 | }; 23 | 24 | // render 25 | return ; 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /docs/stories/options/LightSquareStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/LightSquareStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const LightSquareStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | lightSquareStyle: { 19 | backgroundColor: 'cyan', 20 | }, 21 | id: 'light-square-style', 22 | }; 23 | 24 | // render 25 | return ; 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | // Use single quotes in JavaScript 3 | singleQuote: true, 4 | 5 | // Add trailing commas wherever possible 6 | trailingComma: 'all', 7 | 8 | // Other Prettier default settings 9 | arrowParens: 'always', // Include parens in single param arrow functions 10 | bracketSpacing: true, // Add spaces between brackets in object literals 11 | endOfLine: 'lf', // Maintain line feed for cross-platform consistency 12 | jsxSingleQuote: false, // Use double quotes in JSX attributes 13 | printWidth: 80, // Wrap lines after 80 characters 14 | proseWrap: 'preserve', // Maintain existing line breaks in markdown 15 | semi: true, // Include semicolons at the end of statements 16 | tabWidth: 2, // Set tab width to 2 spaces 17 | useTabs: false, // Use spaces instead of tabs 18 | }; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | assignees: Clariity 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | 21 | **Implementation Notes (Optional)** 22 | If you have any thoughts on how this feature could be implemented, please share them here. 23 | -------------------------------------------------------------------------------- /docs/stories/options/DarkSquareNotationStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/DarkSquareNotationStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const DarkSquareNotationStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | darkSquareNotationStyle: { 19 | color: 'cyan', 20 | fontWeight: 'bold', 21 | }, 22 | id: 'dark-square-notation-style', 23 | }; 24 | 25 | // render 26 | return ; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /docs/stories/options/AlphaNotationStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/AlphaNotationStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const AlphaNotationStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | alphaNotationStyle: { 19 | color: 'cyan', 20 | fontSize: '20px', 21 | fontWeight: 'bold', 22 | }, 23 | id: 'alpha-notation-style', 24 | }; 25 | 26 | // render 27 | return ; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /docs/stories/options/LightSquareNotationStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/LightSquareNotationStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const LightSquareNotationStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | lightSquareNotationStyle: { 19 | color: 'blue', 20 | fontWeight: 'bold', 21 | }, 22 | id: 'light-square-notation-style', 23 | }; 24 | 25 | // render 26 | return ; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /docs/stories/options/NumericNotationStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/NumericNotationStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const NumericNotationStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | numericNotationStyle: { 19 | color: 'cyan', 20 | fontSize: '20px', 21 | fontWeight: 'bold', 22 | }, 23 | id: 'numeric-notation-style', 24 | }; 25 | 26 | // render 27 | return ; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | import remarkGfm from 'remark-gfm'; 3 | 4 | const config: StorybookConfig = { 5 | stories: ['../docs/**/*.mdx', '../docs/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 6 | addons: [ 7 | '@storybook/addon-essentials', 8 | '@chromatic-com/storybook', 9 | '@storybook/addon-interactions', 10 | 'storybook-addon-deep-controls', 11 | { 12 | name: '@storybook/addon-docs', 13 | options: { 14 | mdxPluginOptions: { 15 | mdxCompileOptions: { 16 | remarkPlugins: [remarkGfm], 17 | }, 18 | }, 19 | }, 20 | }, 21 | ], 22 | framework: { 23 | name: '@storybook/react-vite', 24 | options: {}, 25 | }, 26 | staticDirs: ['../docs/assets', '../docs/stockfish'], 27 | docs: { 28 | docsMode: true, 29 | }, 30 | }; 31 | export default config; 32 | -------------------------------------------------------------------------------- /docs/stories/options/SquareStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/SquareStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const SquareStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | squareStyle: { 19 | border: '2px dashed #666', 20 | borderRadius: '8px', 21 | background: 22 | 'linear-gradient(45deg, rgba(255,255,255,0.1), rgba(255,255,255,0.2))', 23 | boxShadow: 'inset 2px 2px 5px rgba(0,0,0,0.1)', 24 | }, 25 | id: 'square-style', 26 | }; 27 | 28 | // render 29 | return ; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/RightClickCancelSensor.tsx: -------------------------------------------------------------------------------- 1 | import { PointerSensor, PointerSensorProps } from '@dnd-kit/core'; 2 | 3 | /** 4 | * A custom PointerSensor that listens for right-clicks during a drag 5 | * and cancels the active drag operation. 6 | * 7 | * Works by listening to the "contextmenu" event on window. 8 | */ 9 | export class RightClickCancelSensor extends PointerSensor { 10 | private handleContextMenu = () => { 11 | // @ts-expect-error: Accessing private props to call onCancel 12 | if (this.props && typeof this.props.onCancel === 'function') { 13 | // @ts-expect-error: Accessing private props to call onCancel 14 | this.props.onCancel(); 15 | } 16 | }; 17 | 18 | constructor(props: PointerSensorProps) { 19 | super(props); 20 | if (typeof window !== 'undefined') { 21 | window.addEventListener('contextmenu', this.handleContextMenu, { 22 | passive: false, 23 | }); 24 | } 25 | } 26 | 27 | teardown() { 28 | if (typeof window !== 'undefined') { 29 | window.removeEventListener('contextmenu', this.handleContextMenu); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/stories/options/BoardStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/BoardStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const BoardStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | boardStyle: { 19 | borderRadius: '10px', 20 | boxShadow: '0 0 10px 0 rgba(0, 0, 0, 0.5)', 21 | border: '1px solid #000', 22 | margin: '20px 0', 23 | }, 24 | id: 'board-style', 25 | }; 26 | 27 | // render 28 | return ( 29 |
36 | 37 |
38 | ); 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ryan Gregory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Draggable.tsx: -------------------------------------------------------------------------------- 1 | import { useDraggable } from '@dnd-kit/core'; 2 | 3 | import { useChessboardContext } from './ChessboardProvider'; 4 | import type { DraggingPieceDataType, PieceDataType } from './types'; 5 | 6 | type DraggableProps = { 7 | children: React.ReactNode; 8 | isSparePiece?: DraggingPieceDataType['isSparePiece']; 9 | pieceType: PieceDataType['pieceType']; 10 | position: DraggingPieceDataType['position']; 11 | }; 12 | 13 | export function Draggable({ 14 | children, 15 | isSparePiece = false, 16 | pieceType, 17 | position, 18 | }: DraggableProps) { 19 | const { allowDragging, canDragPiece } = useChessboardContext(); 20 | 21 | const { setNodeRef, attributes, listeners } = useDraggable({ 22 | id: position, 23 | data: { 24 | isSparePiece, 25 | pieceType, 26 | }, 27 | disabled: 28 | !allowDragging || 29 | (canDragPiece && 30 | !canDragPiece({ 31 | piece: { pieceType }, 32 | isSparePiece, 33 | square: position, 34 | })), 35 | }); 36 | 37 | return ( 38 |
39 | {children} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /docs/stories/options/DropSquareStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/DropSquareStyle', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const DropSquareStyle: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions = { 18 | dropSquareStyle: { 19 | boxShadow: 'inset 0px 0px 0px 5px red', 20 | }, 21 | id: 'drop-square-style', 22 | }; 23 | 24 | // render 25 | return ( 26 |
34 | 35 | 36 |

37 | Drag a piece to see a custom drop square style 38 |

39 |
40 | ); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /docs/components/HintMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type HintMessageProps = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | export function HintMessage({ children }: HintMessageProps) { 8 | return ( 9 |
20 |
21 | 31 | 32 | 33 |
34 | 35 |
36 |

37 | Recommendation 38 |

39 |
{children}
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /docs/stories/options/SquareRenderer.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard, ChessboardOptions } from '../../../src'; 5 | 6 | const meta: Meta = { 7 | ...defaultMeta, 8 | title: 'stories/Options/SquareRenderer', 9 | } satisfies Meta; 10 | 11 | export default meta; 12 | type Story = StoryObj; 13 | 14 | export const SquareRenderer: Story = { 15 | render: () => { 16 | // chessboard options 17 | const chessboardOptions: ChessboardOptions = { 18 | squareRenderer: ({ square, children }) => ( 19 |
26 | {children} 27 | 28 | 35 | {square} 36 | 37 |
38 | ), 39 | id: 'square-renderer', 40 | }; 41 | 42 | // render 43 | return ; 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /docs/stories/options/CanDragPiece.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | import { PieceHandlerArgs } from '../../../src/types'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/CanDragPiece', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const CanDragPiece: Story = { 16 | render: () => { 17 | function canDragPiece({ piece }: PieceHandlerArgs) { 18 | return piece.pieceType[0] === 'w'; 19 | } 20 | 21 | // chessboard options 22 | const chessboardOptions = { 23 | canDragPiece, 24 | id: 'can-drag-piece', 25 | }; 26 | 27 | // render 28 | return ( 29 |
37 | 38 | 39 |

40 | Only white pieces can be dragged 41 |

42 |
43 | ); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /docs/components/WarningMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type WarningMessageProps = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | export function WarningMessage({ children }: WarningMessageProps) { 8 | return ( 9 |
20 |
21 | 31 | 32 | 33 | 34 | 35 |
36 | 37 |
38 |

Warning

39 |
{children}
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: Clariity 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Provide a code sandbox link or easy to follow steps that reproduces the issue 14 | 15 | e.g. Steps to reproduce the behaviour: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behaviour** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screen Recording or Screenshots** 26 | Please add a screen recording or screenshots to help explain your problem. 27 | 28 | **Environment (please complete the following information):** 29 | 30 | - Package version: [e.g. v5.0.0] 31 | - React version [e.g. v19.0.0] 32 | - Node version [e.g. v22.0.0] 33 | 34 | **Desktop (please complete the following information):** 35 | 36 | - OS: [e.g. iOS] 37 | - Browser [e.g. chrome, safari] 38 | - Version [e.g. 22] 39 | 40 | **Smartphone (please complete the following information):** 41 | 42 | - Device: [e.g. iPhone6] 43 | - OS: [e.g. iOS8.1] 44 | - Browser [e.g. stock browser, safari] 45 | - Version [e.g. 22] 46 | 47 | **Additional context** 48 | Add any other context about the problem here. 49 | -------------------------------------------------------------------------------- /docs/stories/options/Pieces.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard, defaultPieces } from '../../../src'; 5 | import type { PieceRenderObject } from '../../../src/types'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/Pieces', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const Pieces: Story = { 16 | render: () => { 17 | const customPieces: PieceRenderObject = { 18 | ...defaultPieces, // exported from react-chessboard 19 | wK: () => ( 20 | 21 | 22 | 23 | ), 24 | bK: () => ( 25 | 26 | 27 | 28 | ), 29 | }; 30 | 31 | // chessboard options 32 | const chessboardOptions = { 33 | pieces: customPieces, 34 | id: 'pieces', 35 | }; 36 | 37 | // render 38 | return ; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Arrow = { 2 | startSquare: string; // e.g. "a8" 3 | endSquare: string; // e.g. "a7" 4 | color: string; // e.g. "#000000" 5 | }; 6 | 7 | export type SquareDataType = { 8 | squareId: string; // e.g. "a8" 9 | isLightSquare: boolean; 10 | }; 11 | 12 | export type PieceDataType = { 13 | pieceType: string; // e.g. "wP" for white pawn, "bK" for black king 14 | }; 15 | 16 | export type DraggingPieceDataType = { 17 | isSparePiece: boolean; 18 | position: string; // e.g. "a8" or "wP" (for spare pieces) 19 | pieceType: string; // e.g. "wP" for white pawn, "bK" for black king 20 | }; 21 | 22 | export type PositionDataType = { 23 | [square: string]: PieceDataType; 24 | }; 25 | 26 | export type SquareHandlerArgs = { 27 | piece: PieceDataType | null; 28 | square: string; 29 | }; 30 | 31 | export type PieceHandlerArgs = { 32 | isSparePiece: boolean; 33 | piece: PieceDataType; 34 | square: string | null; 35 | }; 36 | 37 | export type PieceDropHandlerArgs = { 38 | piece: DraggingPieceDataType; 39 | sourceSquare: string; 40 | targetSquare: string | null; 41 | }; 42 | 43 | export type PieceRenderObject = Record< 44 | string, 45 | (props?: { 46 | fill?: string; 47 | svgStyle?: React.CSSProperties; 48 | }) => React.JSX.Element 49 | >; 50 | 51 | export type FenPieceString = 52 | | 'p' 53 | | 'r' 54 | | 'n' 55 | | 'b' 56 | | 'q' 57 | | 'k' 58 | | 'P' 59 | | 'R' 60 | | 'N' 61 | | 'B' 62 | | 'Q' 63 | | 'K'; 64 | -------------------------------------------------------------------------------- /docs/stories/options/ShowNotation.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/ShowNotation', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const ShowNotation: Story = { 16 | render: () => { 17 | const [showNotation, setShowNotation] = useState(true); 18 | 19 | // chessboard options 20 | const chessboardOptions = { 21 | showNotation, 22 | id: 'show-notation', 23 | }; 24 | 25 | // render 26 | return ( 27 |
35 | 43 | 44 | 45 | 46 |

47 | Toggle the checkbox to show/hide board coordinates 48 |

49 |
50 | ); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Documentation update 15 | - [ ] Performance improvement 16 | - [ ] Code refactoring 17 | 18 | ## How Has This Been Tested? 19 | 20 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 21 | 22 | - [ ] Test A 23 | - [ ] Test B 24 | 25 | ## Checklist: 26 | 27 | - [ ] My code follows the style guidelines of this project 28 | - [ ] I have performed a self-review of my own code 29 | - [ ] I have commented my code, particularly in hard-to-understand areas 30 | - [ ] I have made corresponding changes to the documentation 31 | - [ ] My changes generate no new warnings 32 | - [ ] I have added tests that prove my fix is effective or that my feature works 33 | - [ ] New and existing unit tests pass locally with my changes 34 | - [ ] Any dependent changes have been merged and published in downstream modules 35 | 36 | ## Screenshots (if appropriate): 37 | 38 | ## Additional context 39 | 40 | Add any other context about the pull request here. 41 | -------------------------------------------------------------------------------- /docs/stories/options/AllowDragging.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/AllowDragging', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const AllowDragging: Story = { 16 | render: () => { 17 | const [allowDragging, setAllowDragging] = useState(true); 18 | 19 | // chessboard options 20 | const chessboardOptions = { 21 | allowDragging, 22 | id: 'allow-dragging', 23 | }; 24 | 25 | // render 26 | return ( 27 |
35 | 43 | 44 | 45 | 46 |

47 | Toggle the checkbox to enable/disable piece dragging 48 |

49 |
50 | ); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, beta] 6 | pull_request: 7 | branches: [main, beta] 8 | 9 | jobs: 10 | static-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 9 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Run static analysis 29 | run: pnpm test:static 30 | 31 | semantic-release: 32 | needs: static-test 33 | if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Setup Node.js 41 | uses: actions/setup-node@v4 42 | with: 43 | node-version: '20' 44 | 45 | - name: Setup pnpm 46 | uses: pnpm/action-setup@v2 47 | with: 48 | version: 9 49 | 50 | - name: Install dependencies 51 | run: pnpm install 52 | 53 | - name: Build 54 | run: pnpm build 55 | 56 | - name: Run semantic-release 57 | run: pnpm semantic-release 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | -------------------------------------------------------------------------------- /docs/stories/options/AllowDragOffBoard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/AllowDragOffBoard', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const AllowDragOffBoard: Story = { 16 | render: () => { 17 | const [allowDragOffBoard, setAllowDragOffBoard] = useState(true); 18 | 19 | // chessboard options 20 | const chessboardOptions = { 21 | allowDragOffBoard, 22 | id: 'allow-drag-off-board', 23 | }; 24 | 25 | // render 26 | return ( 27 |
35 | 43 | 44 | 45 | 46 |

47 | Try dragging a piece off the board when the checkbox is unchecked 48 |

49 |
50 | ); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /docs/stories/options/AllowDrawingArrows.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/AllowDrawingArrows', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const AllowDrawingArrows: Story = { 16 | render: () => { 17 | const [allowDrawingArrows, setAllowDrawingArrows] = useState(true); 18 | 19 | // chessboard options 20 | const chessboardOptions = { 21 | allowDrawingArrows, 22 | id: 'allow-drawing-arrows', 23 | }; 24 | 25 | // render 26 | return ( 27 |
35 | 43 | 44 | 45 | 46 |

47 | Toggle the checkbox to enable/disable drawing arrows by holding right 48 | click and dragging 49 |

50 |
51 | ); 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /docs/stories/options/AllowAutoScroll.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/AllowAutoScroll', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const AllowAutoScroll: Story = { 16 | render: () => { 17 | const [allowAutoScroll, setAllowAutoScroll] = useState(false); 18 | 19 | // chessboard options 20 | const chessboardOptions = { 21 | allowAutoScroll, 22 | id: 'allow-auto-scroll', 23 | }; 24 | 25 | // render 26 | return ( 27 |
35 | 43 | 44 | 45 | 46 |

47 | Enable the checkbox and try dragging a piece near the edge of the 48 | screen to see auto-scroll behavior 49 |

50 |
51 | ); 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /docs/stories/options/ClearArrowsOnClick.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/ClearArrowsOnClick', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const ClearArrowsOnClick: Story = { 16 | render: () => { 17 | const [clearArrowsOnClick, setClearArrowsOnClick] = useState(true); 18 | const [arrows] = useState([ 19 | { startSquare: 'e2', endSquare: 'e4', color: 'red' }, 20 | { startSquare: 'g1', endSquare: 'f3', color: 'blue' }, 21 | ]); 22 | 23 | // chessboard options 24 | const chessboardOptions = { 25 | arrows, 26 | clearArrowsOnClick, 27 | id: 'clear-arrows-on-click', 28 | }; 29 | 30 | // render 31 | return ( 32 |
40 | 48 | 49 | 50 | 51 |

52 | Toggle the checkbox to enable/disable clearing arrows when clicking on 53 | a square 54 |

55 |
56 | ); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /docs/stories/options/DragActivationDistance.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/DragActivationDistance', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const DragActivationDistance: Story = { 16 | render: () => { 17 | const [dragActivationDistance, setDragActivationDistance] = useState(2); 18 | 19 | // chessboard options 20 | const chessboardOptions = { 21 | dragActivationDistance, 22 | id: 'drag-activation-distance', 23 | }; 24 | 25 | // render 26 | return ( 27 |
35 | 47 | 48 | 49 | 50 |

51 | Adjust the slider to change how far you need to drag a piece before it 52 | starts moving 53 |

54 |
55 | ); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /docs/stories/options/OnMouseOutSquare.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { SquareHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnMouseOutSquare', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnMouseOutSquare: Story = { 17 | render: () => { 18 | const [lastOutSquare, setLastOutSquare] = useState('None'); 19 | const [lastOutPiece, setLastOutPiece] = useState('None'); 20 | 21 | // handle mouse out square 22 | const onMouseOutSquare = ({ square, piece }: SquareHandlerArgs) => { 23 | setLastOutSquare(square); 24 | setLastOutPiece(piece?.pieceType || null); 25 | }; 26 | 27 | // chessboard options 28 | const chessboardOptions = { 29 | onMouseOutSquare, 30 | id: 'on-mouse-out-square', 31 | }; 32 | 33 | // render 34 | return ( 35 |
43 |
44 | Last square mouse left: {lastOutSquare} 45 |
46 | Piece in last square mouse left: {lastOutPiece} 47 |
48 | 49 | 50 | 51 |

52 | Move your mouse over and out of squares to see the mouse out events 53 |

54 |
55 | ); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /docs/stories/options/OnMouseOverSquare.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { SquareHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnMouseOverSquare', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnMouseOverSquare: Story = { 17 | render: () => { 18 | const [lastOverSquare, setLastOverSquare] = useState('None'); 19 | const [lastOverPiece, setLastOverPiece] = useState('None'); 20 | 21 | // handle mouse over square 22 | const onMouseOverSquare = ({ square, piece }: SquareHandlerArgs) => { 23 | setLastOverSquare(square); 24 | setLastOverPiece(piece?.pieceType || null); 25 | }; 26 | 27 | // chessboard options 28 | const chessboardOptions = { 29 | onMouseOverSquare, 30 | id: 'on-mouse-over-square', 31 | }; 32 | 33 | // render 34 | return ( 35 |
43 |
44 | Last square mouse entered: {lastOverSquare} 45 |
46 | Piece in last square mouse entered: {lastOverPiece} 47 |
48 | 49 | 50 | 51 |

52 | Move your mouse over squares to see the mouse over events 53 |

54 |
55 | ); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /docs/stories/options/OnSquareClick.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { SquareHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnSquareClick', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnSquareClick: Story = { 17 | render: () => { 18 | const [clickedSquare, setClickedSquare] = useState(null); 19 | const [clickedPiece, setClickedPiece] = useState(null); 20 | 21 | // handle square click 22 | const onSquareClick = ({ square, piece }: SquareHandlerArgs) => { 23 | setClickedSquare(square); 24 | setClickedPiece(piece?.pieceType || null); 25 | }; 26 | 27 | // chessboard options 28 | const chessboardOptions = { 29 | onSquareClick, 30 | id: 'on-square-click', 31 | }; 32 | 33 | // render 34 | return ( 35 |
43 |
46 |
Clicked square: {clickedSquare || 'None'}
47 |
Piece in clicked square: {clickedPiece || 'None'}
48 |
49 | 50 | 51 | 52 |

53 | Click on squares to see the click events in action 54 |

55 |
56 | ); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /docs/stories/options/BoardOrientation.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/BoardOrientation', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const BoardOrientation: Story = { 16 | render: () => { 17 | const [boardOrientation, setBoardOrientation] = useState<'white' | 'black'>( 18 | 'white', 19 | ); 20 | 21 | // chessboard options 22 | const chessboardOptions = { 23 | boardOrientation, 24 | id: 'board-orientation', 25 | }; 26 | 27 | // render 28 | return ( 29 |
37 |
38 | 46 | 54 |
55 | 56 | 57 | 58 |

59 | Toggle the radio buttons to change the board orientation 60 |

61 |
62 | ); 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /docs/stories/options/OnPieceDrag.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { PieceHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnPieceDrag', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnPieceDrag: Story = { 17 | render: () => { 18 | const [draggedSquare, setDraggedSquare] = useState('None'); 19 | const [draggedPiece, setDraggedPiece] = useState('None'); 20 | const [isSparePiece, setIsSparePiece] = useState(false); 21 | 22 | // handle piece drag start 23 | const onPieceDrag = ({ square, piece, isSparePiece }: PieceHandlerArgs) => { 24 | setDraggedSquare(square || 'None'); 25 | setDraggedPiece(piece.pieceType); 26 | setIsSparePiece(isSparePiece); 27 | }; 28 | 29 | // chessboard options 30 | const chessboardOptions = { 31 | onPieceDrag, 32 | id: 'on-piece-drag-start', 33 | }; 34 | 35 | // render 36 | return ( 37 |
45 |
46 | Dragged from square: {draggedSquare} 47 |
48 | Dragged piece: {draggedPiece} 49 |
50 | Is spare piece: {isSparePiece ? 'Yes' : 'No'} 51 |
52 | 53 | 54 | 55 |

56 | Start dragging pieces to see the drag start events 57 |

58 |
59 | ); 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /docs/stories/options/OnPieceClick.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { PieceHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnPieceClick', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnPieceClick: Story = { 17 | render: () => { 18 | const [clickedSquare, setClickedSquare] = useState('None'); 19 | const [clickedPiece, setClickedPiece] = useState('None'); 20 | const [isSparePiece, setIsSparePiece] = useState(false); 21 | 22 | // handle piece click 23 | const onPieceClick = ({ 24 | square, 25 | piece, 26 | isSparePiece, 27 | }: PieceHandlerArgs) => { 28 | setClickedSquare(square || 'None'); 29 | setClickedPiece(piece.pieceType); 30 | setIsSparePiece(isSparePiece); 31 | }; 32 | 33 | // chessboard options 34 | const chessboardOptions = { 35 | allowDragging: false, 36 | onPieceClick, 37 | id: 'on-piece-click', 38 | }; 39 | 40 | // render 41 | return ( 42 |
50 |
51 | Clicked square: {clickedSquare} 52 |
53 | Clicked piece: {clickedPiece} 54 |
55 | Is spare piece: {isSparePiece ? 'Yes' : 'No'} 56 |
57 | 58 | 59 | 60 |

61 | Click on pieces to see the click events 62 |

63 |
64 | ); 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /docs/stories/options/ChessboardRows.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import defaultMeta from '../basic-examples/Default.stories'; 4 | import { Chessboard } from '../../../src'; 5 | import { PositionDataType } from '../../../src/types'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/ChessboardRows', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const ChessboardRows: Story = { 16 | render: () => { 17 | // chessboard options 18 | const chessboardOptions = { 19 | chessboardRows: 16, 20 | position: { 21 | a1: { pieceType: 'wR' }, 22 | a2: { pieceType: 'wP' }, 23 | a15: { pieceType: 'bP' }, 24 | a16: { pieceType: 'bR' }, 25 | b1: { pieceType: 'wN' }, 26 | b2: { pieceType: 'wP' }, 27 | b15: { pieceType: 'bP' }, 28 | b16: { pieceType: 'bN' }, 29 | c1: { pieceType: 'wB' }, 30 | c2: { pieceType: 'wP' }, 31 | c15: { pieceType: 'bP' }, 32 | c16: { pieceType: 'bB' }, 33 | d1: { pieceType: 'wQ' }, 34 | d2: { pieceType: 'wP' }, 35 | d15: { pieceType: 'bP' }, 36 | d16: { pieceType: 'bQ' }, 37 | e1: { pieceType: 'wK' }, 38 | e2: { pieceType: 'wP' }, 39 | e15: { pieceType: 'bP' }, 40 | e16: { pieceType: 'bK' }, 41 | f1: { pieceType: 'wB' }, 42 | f2: { pieceType: 'wP' }, 43 | f15: { pieceType: 'bP' }, 44 | f16: { pieceType: 'bB' }, 45 | g1: { pieceType: 'wN' }, 46 | g2: { pieceType: 'wP' }, 47 | g15: { pieceType: 'bP' }, 48 | g16: { pieceType: 'bN' }, 49 | h1: { pieceType: 'wR' }, 50 | h2: { pieceType: 'wP' }, 51 | h15: { pieceType: 'bP' }, 52 | h16: { pieceType: 'bR' }, 53 | } as PositionDataType, 54 | id: 'chessboard-rows', 55 | }; 56 | 57 | // render 58 | return ; 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /docs/stories/options/OnSquareRightClick.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { SquareHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnSquareRightClick', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnSquareRightClick: Story = { 17 | render: () => { 18 | const [rightClickedSquare, setRightClickedSquare] = useState( 19 | null, 20 | ); 21 | const [rightClickedPiece, setRightClickedPiece] = useState( 22 | null, 23 | ); 24 | 25 | // handle square right click 26 | const onSquareRightClick = ({ square, piece }: SquareHandlerArgs) => { 27 | setRightClickedSquare(square); 28 | setRightClickedPiece(piece?.pieceType || null); 29 | }; 30 | 31 | // chessboard options 32 | const chessboardOptions = { 33 | onSquareRightClick, 34 | id: 'on-square-right-click', 35 | }; 36 | 37 | // render 38 | return ( 39 |
47 |
50 |
Right-clicked square: {rightClickedSquare || 'None'}
51 |
52 | Piece in right-clicked square: {rightClickedPiece || 'None'} 53 |
54 |
55 | 56 | 57 | 58 |

59 | Right-click on squares to see the right-click events in action 60 |

61 |
62 | ); 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/modifiers.ts: -------------------------------------------------------------------------------- 1 | import { Modifier } from '@dnd-kit/core'; 2 | 3 | export const preventDragOffBoard = ( 4 | boardId: string, 5 | draggingPiecePosition: string, 6 | ): Modifier => { 7 | return ({ transform }) => { 8 | const boardElement = 9 | typeof document !== 'undefined' 10 | ? document.getElementById(`${boardId}-board`) 11 | : null; 12 | 13 | if (!boardElement) { 14 | return transform; 15 | } 16 | 17 | // Get the a1 square to determine square size using data attributes 18 | const boardRect = boardElement.getBoundingClientRect(); 19 | const a1Square = boardElement.querySelector( 20 | '[data-column="a"][data-row="1"]', 21 | ); 22 | 23 | if (!a1Square) { 24 | return transform; 25 | } 26 | 27 | const squareWidth = a1Square.getBoundingClientRect().width; 28 | const halfSquareWidth = squareWidth / 2; 29 | 30 | // Extract column and row from position (supports multi-char columns/rows) 31 | const match = draggingPiecePosition.match(/^([a-zA-Z]+)(\d+)$/); 32 | if (!match) { 33 | return transform; 34 | } 35 | const [, col, row] = match; 36 | 37 | // Get the starting position of the piece 38 | const startSquare = boardElement.querySelector( 39 | `[data-column="${col}"][data-row="${row}"]`, 40 | ); 41 | 42 | if (!startSquare) { 43 | return transform; 44 | } 45 | 46 | const startSquareRect = startSquare.getBoundingClientRect(); 47 | const startX = startSquareRect.left + halfSquareWidth - boardRect.left; 48 | const startY = startSquareRect.top + halfSquareWidth - boardRect.top; 49 | 50 | // Clamp so the center of the piece can go exactly half a square width outside the board 51 | const minX = -startX; 52 | const maxX = boardRect.width - startX; 53 | const minY = -startY; 54 | const maxY = boardRect.height - startY; 55 | 56 | const clampedX = Math.min(Math.max(transform.x, minX), maxX); 57 | const clampedY = Math.min(Math.max(transform.y, minY), maxY); 58 | 59 | return { 60 | ...transform, 61 | x: clampedX, 62 | y: clampedY, 63 | }; 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /docs/stories/options/SquareStyles.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import { SquareHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/SquareStyles', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const SquareStyles: Story = { 17 | render: () => { 18 | const [squareStyles, setSquareStyles] = useState< 19 | Record 20 | >({ 21 | e4: { 22 | backgroundColor: 'rgba(255,0,0,0.2)', 23 | }, 24 | }); 25 | 26 | function onSquareClick() { 27 | setSquareStyles({}); 28 | } 29 | 30 | // add or remove a style when a square is right clicked 31 | function onSquareRightClick(args: SquareHandlerArgs) { 32 | setSquareStyles((prev) => { 33 | const newSquareStyles = { ...prev }; 34 | if (newSquareStyles[args.square]) { 35 | delete newSquareStyles[args.square]; 36 | } else { 37 | newSquareStyles[args.square] = { 38 | backgroundColor: 'rgba(255,0,0,0.2)', 39 | }; 40 | } 41 | return newSquareStyles; 42 | }); 43 | } 44 | 45 | // chessboard options 46 | const chessboardOptions = { 47 | onSquareClick, 48 | onSquareRightClick, 49 | squareStyles, 50 | id: 'square-styles', 51 | }; 52 | 53 | // render 54 | return ( 55 |
63 | 64 | 65 |

66 | Right click on a square to add or remove a red background. Left click 67 | to remove all red backgrounds. 68 |

69 |
70 | ); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /docs/stories/options/OnPieceDrop.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { PieceDropHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnPieceDrop', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnPieceDrop: Story = { 17 | render: () => { 18 | const [sourceSquare, setSourceSquare] = useState('None'); 19 | const [targetSquare, setTargetSquare] = useState('None'); 20 | const [droppedPiece, setDroppedPiece] = useState('None'); 21 | const [isSparePiece, setIsSparePiece] = useState(false); 22 | 23 | // handle piece drop 24 | const onPieceDrop = ({ 25 | sourceSquare, 26 | targetSquare, 27 | piece, 28 | }: PieceDropHandlerArgs) => { 29 | setSourceSquare(sourceSquare); 30 | setTargetSquare(targetSquare || 'None'); 31 | setDroppedPiece(piece.pieceType); 32 | setIsSparePiece(piece.isSparePiece); 33 | return true; // Allow the drop 34 | }; 35 | 36 | // chessboard options 37 | const chessboardOptions = { 38 | onPieceDrop, 39 | id: 'on-piece-drop', 40 | }; 41 | 42 | // render 43 | return ( 44 |
52 |
53 | Source square: {sourceSquare} 54 |
55 | Target square: {targetSquare} 56 |
57 | Dropped piece: {droppedPiece} 58 |
59 | Is spare piece: {isSparePiece ? 'Yes' : 'No'} 60 |
61 | 62 | 63 | 64 |

65 | Drag and drop pieces to see the drop events 66 |

67 |
68 | ); 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /docs/stories/options/OnSquareMouseUp.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { SquareHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnSquareClick', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnSquareMouseUp: Story = { 17 | render: () => { 18 | const [mouseUpSquare, setMouseUpSquare] = useState(null); 19 | const [mouseUpPiece, setMouseUpPiece] = useState(null); 20 | const [buttonReleased, setButtonReleased] = useState(null); 21 | 22 | // handle square click 23 | const onSquareMouseUp = ( 24 | { square, piece }: SquareHandlerArgs, 25 | e: React.MouseEvent, 26 | ) => { 27 | setMouseUpSquare(square); 28 | setMouseUpPiece(piece?.pieceType || null); 29 | setButtonReleased( 30 | e.button === 0 31 | ? 'Left' 32 | : e.button === 1 33 | ? 'Middle' 34 | : e.button === 2 35 | ? 'Right' 36 | : `Button ${e.button}`, 37 | ); 38 | }; 39 | 40 | // chessboard options 41 | const chessboardOptions = { 42 | onSquareMouseUp, 43 | id: 'on-square-mouse-up', 44 | }; 45 | 46 | // render 47 | return ( 48 |
56 |
59 |
Mouse released in square: {mouseUpSquare || 'None'}
60 |
Piece in square: {mouseUpPiece || 'None'}
61 |
Button released: {buttonReleased || 'None'}
62 |
63 | 64 | 65 | 66 |

67 | Release mouse button on squares to see the mouse up events in action 68 |

69 |
70 | ); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /docs/stories/options/DraggingPieceStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/DraggingPieceStyle', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const DraggingPieceStyle: Story = { 16 | render: () => { 17 | const [scale, setScale] = useState(1.2); 18 | const [rotate, setRotate] = useState(0); 19 | 20 | // chessboard options 21 | const chessboardOptions = { 22 | draggingPieceStyle: { 23 | transform: `scale(${scale}) rotate(${rotate}deg)`, 24 | }, 25 | id: 'dragging-piece-style', 26 | }; 27 | 28 | // render 29 | return ( 30 |
38 |
39 | 51 | 52 | 64 |
65 | 66 | 67 | 68 |

69 | Drag a piece to see the custom dragging style. Adjust the sliders to 70 | change the scale and rotation of the dragged piece. 71 |

72 |
73 | ); 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/Board.tsx: -------------------------------------------------------------------------------- 1 | import { DragOverlay } from '@dnd-kit/core'; 2 | import { snapCenterToCursor } from '@dnd-kit/modifiers'; 3 | 4 | import { Arrows } from './Arrows'; 5 | import { Draggable } from './Draggable'; 6 | import { Droppable } from './Droppable'; 7 | import { Piece } from './Piece'; 8 | import { Square } from './Square'; 9 | import { useChessboardContext } from './ChessboardProvider'; 10 | import { defaultBoardStyle } from './defaults'; 11 | import { preventDragOffBoard } from './modifiers'; 12 | 13 | export function Board() { 14 | const { 15 | allowDragOffBoard, 16 | board, 17 | boardStyle, 18 | chessboardColumns, 19 | currentPosition, 20 | draggingPiece, 21 | id, 22 | } = useChessboardContext(); 23 | 24 | return ( 25 | <> 26 |
30 | {board.map((row) => 31 | row.map((square) => { 32 | const piece = currentPosition[square.squareId]; 33 | 34 | return ( 35 | 36 | {({ isOver }) => ( 37 | 38 | {piece ? ( 39 | 44 | 45 | 46 | ) : null} 47 | 48 | )} 49 | 50 | ); 51 | }), 52 | )} 53 | 54 | 55 |
56 | 57 | 66 | {draggingPiece ? ( 67 | 72 | ) : null} 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /docs/stories/options/DraggingPieceGhostStyle.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/DraggingPieceGhostStyle', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const DraggingPieceGhostStyle: Story = { 16 | render: () => { 17 | const [opacity, setOpacity] = useState(0.5); 18 | const [blur, setBlur] = useState(0); 19 | 20 | // chessboard options 21 | const chessboardOptions = { 22 | draggingPieceGhostStyle: { 23 | opacity, 24 | filter: `blur(${blur}px)`, 25 | }, 26 | id: 'dragging-piece-ghost-style', 27 | }; 28 | 29 | // render 30 | return ( 31 |
39 |
40 | 52 | 53 | 65 |
66 | 67 | 68 | 69 |

70 | Drag a piece to see the ghost piece style. Adjust the sliders to 71 | change the opacity and blur of the ghost piece. 72 |

73 |
74 | ); 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /docs/stories/options/OnSquareMouseDown.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import React, { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { SquareHandlerArgs } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnSquareClick', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnSquareMouseDown: Story = { 17 | render: () => { 18 | const [mouseDownSquare, setMouseDownSquare] = useState(null); 19 | const [mouseDownPiece, setMouseDownPiece] = useState(null); 20 | const [buttonPressed, setButtonPressed] = useState(null); 21 | 22 | // handle square click 23 | const onSquareMouseDown = ( 24 | { square, piece }: SquareHandlerArgs, 25 | e: React.MouseEvent, 26 | ) => { 27 | setMouseDownSquare(square); 28 | setMouseDownPiece(piece?.pieceType || null); 29 | setButtonPressed( 30 | e.button === 0 31 | ? 'Left' 32 | : e.button === 1 33 | ? 'Middle' 34 | : e.button === 2 35 | ? 'Right' 36 | : `Button ${e.button}`, 37 | ); 38 | }; 39 | 40 | // chessboard options 41 | const chessboardOptions = { 42 | onSquareMouseDown, 43 | id: 'on-square-mouse-down', 44 | }; 45 | 46 | // render 47 | return ( 48 |
56 |
59 |
Mouse last pressed in: {mouseDownSquare || 'None'}
60 |
Piece in square: {mouseDownPiece || 'None'}
61 |
Button pressed: {buttonPressed || 'None'}
62 |
63 | 64 | 65 | 66 |

67 | Press mouse button down on squares to see the mouse down events in 68 | action 69 |

70 |
71 | ); 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /docs/stories/options/Arrows.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/Arrows', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | // Helper function to generate random square 16 | const getRandomSquare = () => { 17 | const files = 'abcdefgh'; 18 | const ranks = '12345678'; 19 | return `${files[Math.floor(Math.random() * 8)]}${ranks[Math.floor(Math.random() * 8)]}`; 20 | }; 21 | 22 | // Helper function to generate unique squares 23 | const getUniqueSquares = (count: number) => { 24 | const squares = new Set(); 25 | while (squares.size < count) { 26 | squares.add(getRandomSquare()); 27 | } 28 | return Array.from(squares); 29 | }; 30 | 31 | export const Arrows: Story = { 32 | render: () => { 33 | const [arrows, setArrows] = useState([ 34 | { startSquare: 'e2', endSquare: 'e4', color: 'red' }, 35 | { startSquare: 'g1', endSquare: 'f3', color: 'blue' }, 36 | { startSquare: 'c1', endSquare: 'g5', color: 'green' }, 37 | ]); 38 | 39 | const generateRandomArrows = () => { 40 | // Get 6 unique squares (3 pairs of start/end squares) 41 | const uniqueSquares = getUniqueSquares(6); 42 | const colors = ['red', 'blue', 'green']; 43 | 44 | const newArrows = Array.from({ length: 3 }, (_, index) => ({ 45 | startSquare: uniqueSquares[index * 2], 46 | endSquare: uniqueSquares[index * 2 + 1], 47 | color: colors[index], 48 | })); 49 | 50 | setArrows(newArrows); 51 | }; 52 | 53 | // chessboard options 54 | const chessboardOptions = { 55 | arrows, 56 | id: 'arrows', 57 | }; 58 | 59 | // render 60 | return ( 61 |
69 | 70 | 71 | 72 | 73 |

74 | Click the button to generate 3 random arrows on the board. 75 |

76 |
77 | ); 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /src/defaults.ts: -------------------------------------------------------------------------------- 1 | export function defaultBoardStyle( 2 | chessboardColumns: number, 3 | ): React.CSSProperties { 4 | return { 5 | display: 'grid', 6 | gridTemplateColumns: `repeat(${chessboardColumns}, 1fr)`, 7 | overflow: 'hidden', 8 | width: '100%', 9 | height: '100%', 10 | position: 'relative', 11 | }; 12 | } 13 | 14 | export const defaultSquareStyle: React.CSSProperties = { 15 | aspectRatio: '1/1', 16 | display: 'flex', 17 | justifyContent: 'center', 18 | alignItems: 'center', 19 | position: 'relative', 20 | }; 21 | 22 | export const defaultDarkSquareStyle: React.CSSProperties = { 23 | backgroundColor: '#B58863', 24 | }; 25 | 26 | export const defaultLightSquareStyle: React.CSSProperties = { 27 | backgroundColor: '#F0D9B5', 28 | }; 29 | 30 | export const defaultDropSquareStyle: React.CSSProperties = { 31 | boxShadow: 'inset 0px 0px 0px 1px black', 32 | }; 33 | 34 | export const defaultDarkSquareNotationStyle: React.CSSProperties = { 35 | color: '#F0D9B5', 36 | }; 37 | 38 | export const defaultLightSquareNotationStyle: React.CSSProperties = { 39 | color: '#B58863', 40 | }; 41 | 42 | export const defaultAlphaNotationStyle: React.CSSProperties = { 43 | fontSize: '13px', 44 | position: 'absolute', 45 | bottom: 1, 46 | right: 4, 47 | userSelect: 'none', 48 | }; 49 | 50 | export const defaultNumericNotationStyle: React.CSSProperties = { 51 | fontSize: '13px', 52 | position: 'absolute', 53 | top: 2, 54 | left: 2, 55 | userSelect: 'none', 56 | }; 57 | 58 | export const defaultDraggingPieceStyle: React.CSSProperties = { 59 | transform: 'scale(1.2)', 60 | }; 61 | 62 | export const defaultDraggingPieceGhostStyle: React.CSSProperties = { 63 | opacity: 0.5, 64 | }; 65 | 66 | export const defaultArrowOptions = { 67 | color: '#ffaa00', // color if no modifiers are held down when drawing an arrow 68 | secondaryColor: '#4caf50', // color if shift is held down when drawing an arrow 69 | tertiaryColor: '#f44336', // color if control is held down when drawing an arrow 70 | arrowLengthReducerDenominator: 8, // the lower the denominator, the greater the arrow length reduction (e.g. 8 = 1/8 of a square width removed, 4 = 1/4 of a square width removed) 71 | sameTargetArrowLengthReducerDenominator: 4, // as above but for arrows targeting the same square (a greater reduction is used to avoid overlaps) 72 | arrowWidthDenominator: 5, // the lower the denominator, the greater the arrow width (e.g. 5 = 1/5 of a square width, 10 = 1/10 of a square width) 73 | activeArrowWidthMultiplier: 0.9, // the multiplier for the arrow width when it is being drawn 74 | opacity: 0.65, // opacity of arrow when not being drawn 75 | activeOpacity: 0.5, // opacity of arrow when it is being drawn 76 | }; 77 | -------------------------------------------------------------------------------- /docs/stories/basic-examples/PlayVsRandom.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess } from 'chess.js'; 3 | import { useState, useRef } from 'react'; 4 | 5 | import defaultMeta from './Default.stories'; 6 | import { Chessboard, PieceDropHandlerArgs } from '../../../src'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/PlayVsRandom', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const PlayVsRandom: Story = { 18 | render: () => { 19 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 20 | const chessGameRef = useRef(new Chess()); 21 | const chessGame = chessGameRef.current; 22 | 23 | // track the current position of the chess game in state to trigger a re-render of the chessboard 24 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 25 | 26 | // make a random "CPU" move 27 | function makeRandomMove() { 28 | // get all possible moves` 29 | const possibleMoves = chessGame.moves(); 30 | 31 | // exit if the game is over 32 | if (chessGame.isGameOver()) { 33 | return; 34 | } 35 | 36 | // pick a random move 37 | const randomMove = 38 | possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; 39 | 40 | // make the move 41 | chessGame.move(randomMove); 42 | 43 | // update the position state 44 | setChessPosition(chessGame.fen()); 45 | } 46 | 47 | // handle piece drop 48 | function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) { 49 | // type narrow targetSquare potentially being null (e.g. if dropped off board) 50 | if (!targetSquare) { 51 | return false; 52 | } 53 | 54 | // try to make the move according to chess.js logic 55 | try { 56 | chessGame.move({ 57 | from: sourceSquare, 58 | to: targetSquare, 59 | promotion: 'q', // always promote to a queen for example simplicity 60 | }); 61 | 62 | // update the position state upon successful move to trigger a re-render of the chessboard 63 | setChessPosition(chessGame.fen()); 64 | 65 | // make random cpu move after a short delay 66 | setTimeout(makeRandomMove, 500); 67 | 68 | // return true as the move was successful 69 | return true; 70 | } catch { 71 | // return false as the move was not successful 72 | return false; 73 | } 74 | } 75 | 76 | // set the chessboard options 77 | const chessboardOptions = { 78 | position: chessPosition, 79 | onPieceDrop, 80 | id: 'play-vs-random', 81 | }; 82 | 83 | // render the chessboard 84 | return ; 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /docs/stockfish/engine.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Stockfish.js (http://github.com/nmrugg/stockfish.js) 3 | * License: GPL 4 | */ 5 | 6 | /* 7 | * Description of the universal chess interface (UCI) https://gist.github.com/aliostad/f4470274f39d29b788c1b09519e67372/ 8 | */ 9 | 10 | const stockfish = new Worker('./stockfish.wasm.js'); 11 | 12 | type EngineMessage = { 13 | /** stockfish engine message in UCI format*/ 14 | uciMessage: string; 15 | /** found best move for current position in format `e2e4`*/ 16 | bestMove?: string; 17 | /** found best move for opponent in format `e7e5` */ 18 | ponder?: string; 19 | /** material balance's difference in centipawns(IMPORTANT! stockfish gives the cp score in terms of whose turn it is)*/ 20 | positionEvaluation?: string; 21 | /** count of moves until mate */ 22 | possibleMate?: string; 23 | /** the best line found */ 24 | pv?: string; 25 | /** number of halfmoves the engine looks ahead */ 26 | depth?: number; 27 | }; 28 | 29 | export default class Engine { 30 | stockfish: Worker; 31 | onMessage: (callback: (messageData: EngineMessage) => void) => void; 32 | isReady: boolean; 33 | 34 | constructor() { 35 | this.stockfish = stockfish; 36 | this.isReady = false; 37 | this.onMessage = (callback) => { 38 | this.stockfish.addEventListener('message', (e) => { 39 | callback(this.transformSFMessageData(e)); 40 | }); 41 | }; 42 | this.init(); 43 | } 44 | 45 | private transformSFMessageData(e: MessageEvent) { 46 | const uciMessage = e?.data ?? e; 47 | 48 | return { 49 | uciMessage, 50 | bestMove: uciMessage.match(/bestmove\s+(\S+)/)?.[1], 51 | ponder: uciMessage.match(/ponder\s+(\S+)/)?.[1], 52 | positionEvaluation: uciMessage.match(/cp\s+(\S+)/)?.[1], 53 | possibleMate: uciMessage.match(/mate\s+(\S+)/)?.[1], 54 | pv: uciMessage.match(/ pv\s+(.*)/)?.[1], 55 | depth: Number(uciMessage.match(/ depth\s+(\S+)/)?.[1] ?? 0), 56 | }; 57 | } 58 | 59 | init() { 60 | this.stockfish.postMessage('uci'); 61 | this.stockfish.postMessage('isready'); 62 | this.onMessage(({ uciMessage }) => { 63 | if (uciMessage === 'readyok') { 64 | this.isReady = true; 65 | } 66 | }); 67 | } 68 | 69 | onReady(callback: () => void) { 70 | this.onMessage(({ uciMessage }) => { 71 | if (uciMessage === 'readyok') { 72 | callback(); 73 | } 74 | }); 75 | } 76 | 77 | evaluatePosition(fen: string, depth = 12) { 78 | if (depth > 24) depth = 24; 79 | 80 | this.stockfish.postMessage(`position fen ${fen}`); 81 | this.stockfish.postMessage(`go depth ${depth}`); 82 | } 83 | 84 | stop() { 85 | this.stockfish.postMessage('stop'); // Run when searching takes too long time and stockfish will return you the bestmove of the deep it has reached 86 | } 87 | 88 | terminate() { 89 | this.isReady = false; 90 | this.stockfish.postMessage('quit'); // Run this before chessboard unmounting. 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-chessboard", 3 | "version": "0.0.0-semantically-released", 4 | "description": "The React Chessboard Library", 5 | "author": "Ryan Gregory ", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "types": "dist/index.d.ts", 10 | "type": "module", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/index.esm.js", 15 | "require": "./dist/index.js" 16 | } 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "packageManager": "pnpm@9.4.0", 22 | "engines": { 23 | "node": ">=20.11.0", 24 | "pnpm": ">=9.4.0" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Clariity/react-chessboard.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/Clariity/react-chessboard/issues" 32 | }, 33 | "homepage": "https://github.com/Clariity/react-chessboard#readme", 34 | "scripts": { 35 | "build": "rimraf dist && rollup -c", 36 | "test:static": "tsc && eslint src docs && prettier --check src docs", 37 | "format": "eslint --fix src docs && prettier --write src docs", 38 | "prettier:fix": "prettier --write src docs", 39 | "commitlint": "commitlint --edit", 40 | "storybook": "storybook dev -p 6006", 41 | "build-storybook": "storybook build", 42 | "prepare": "husky" 43 | }, 44 | "dependencies": { 45 | "@dnd-kit/core": "^6.3.1", 46 | "@dnd-kit/modifiers": "^9.0.0" 47 | }, 48 | "devDependencies": { 49 | "@chromatic-com/storybook": "3", 50 | "@commitlint/cli": "^19.8.1", 51 | "@commitlint/config-conventional": "^19.8.1", 52 | "@eslint/js": "^9.26.0", 53 | "@rollup/plugin-commonjs": "^28.0.3", 54 | "@rollup/plugin-node-resolve": "^16.0.1", 55 | "@storybook/addon-docs": "^8.6.8", 56 | "@storybook/addon-essentials": "8.6.8", 57 | "@storybook/addon-interactions": "8.6.8", 58 | "@storybook/blocks": "8.6.8", 59 | "@storybook/manager-api": "8.6.8", 60 | "@storybook/react": "8.6.8", 61 | "@storybook/react-vite": "8.6.8", 62 | "@storybook/test": "8.6.8", 63 | "@storybook/theming": "8.6.8", 64 | "@types/node": "^22.13.11", 65 | "@types/react": "^19.0.12", 66 | "@types/react-dom": "^19.0.4", 67 | "chess.js": "^1.2.0", 68 | "conventional-changelog-conventionalcommits": "^9.0.0", 69 | "eslint": "^9.26.0", 70 | "eslint-config-prettier": "^10.1.5", 71 | "eslint-plugin-react": "^7.37.5", 72 | "globals": "^16.1.0", 73 | "husky": "^9.1.7", 74 | "prettier": "^3.5.3", 75 | "react": "^19.0.0", 76 | "react-dom": "^19.0.0", 77 | "remark-gfm": "^4.0.1", 78 | "rimraf": "^6.0.1", 79 | "rollup": "^4.42.0", 80 | "rollup-plugin-typescript2": "^0.36.0", 81 | "semantic-release": "^24.2.5", 82 | "storybook": "8.6.8", 83 | "storybook-addon-deep-controls": "^0.9.2", 84 | "typescript": "^5.8.2", 85 | "typescript-eslint": "^8.32.0" 86 | }, 87 | "peerDependencies": { 88 | "react": "^19.0.0", 89 | "react-dom": "^19.0.0" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docs/stories/options/Position.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/Position', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const Position: Story = { 16 | render: () => { 17 | const [showAnimations, setShowAnimations] = useState(true); 18 | const [position, setPosition] = useState( 19 | 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', 20 | ); 21 | 22 | // generate random FEN position 23 | function generateRandomFen() { 24 | const pieces = [ 25 | 'r', 26 | 'n', 27 | 'b', 28 | 'q', 29 | 'k', 30 | 'p', 31 | 'R', 32 | 'N', 33 | 'B', 34 | 'Q', 35 | 'K', 36 | 'P', 37 | ]; 38 | let fen = ''; 39 | 40 | // create 8 rows of random pieces 41 | for (let i = 0; i < 8; i++) { 42 | let emptyCount = 0; 43 | 44 | // create 8 columns of random pieces or empty squares 45 | for (let j = 0; j < 8; j++) { 46 | if (Math.random() < 0.2) { 47 | if (emptyCount > 0) { 48 | fen += emptyCount; 49 | emptyCount = 0; 50 | } 51 | 52 | fen += pieces[Math.floor(Math.random() * pieces.length)]; 53 | } else { 54 | emptyCount++; 55 | } 56 | } 57 | 58 | // add empty count to FEN string if there are empty squares 59 | if (emptyCount > 0) { 60 | fen += emptyCount; 61 | } 62 | 63 | // add slash between rows 64 | if (i < 7) { 65 | fen += '/'; 66 | } 67 | } 68 | 69 | // set the position 70 | setPosition(fen); 71 | } 72 | 73 | // chessboard options 74 | const chessboardOptions = { 75 | position, 76 | showAnimations, 77 | id: 'position', 78 | }; 79 | 80 | // render 81 | return ( 82 |
90 | 93 | 101 |

{position}

102 | 103 | 104 | 105 |

106 | Click on the button to generate a random FEN position 107 |

108 |
109 | ); 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /docs/stories/options/ShowAnimations.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/ShowAnimations', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const ShowAnimations: Story = { 16 | render: () => { 17 | const [showAnimations, setShowAnimations] = useState(true); 18 | const [position, setPosition] = useState( 19 | 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', 20 | ); 21 | 22 | // generate random FEN position 23 | function generateRandomFen() { 24 | const pieces = [ 25 | 'r', 26 | 'n', 27 | 'b', 28 | 'q', 29 | 'k', 30 | 'p', 31 | 'R', 32 | 'N', 33 | 'B', 34 | 'Q', 35 | 'K', 36 | 'P', 37 | ]; 38 | let fen = ''; 39 | 40 | // create 8 rows of random pieces 41 | for (let i = 0; i < 8; i++) { 42 | let emptyCount = 0; 43 | 44 | // create 8 columns of random pieces or empty squares 45 | for (let j = 0; j < 8; j++) { 46 | if (Math.random() < 0.2) { 47 | if (emptyCount > 0) { 48 | fen += emptyCount; 49 | emptyCount = 0; 50 | } 51 | 52 | fen += pieces[Math.floor(Math.random() * pieces.length)]; 53 | } else { 54 | emptyCount++; 55 | } 56 | } 57 | 58 | // add empty count to FEN string if there are empty squares 59 | if (emptyCount > 0) { 60 | fen += emptyCount; 61 | } 62 | 63 | // add slash between rows 64 | if (i < 7) { 65 | fen += '/'; 66 | } 67 | } 68 | 69 | // set the position 70 | setPosition(fen); 71 | } 72 | 73 | // chessboard options 74 | const chessboardOptions = { 75 | allowDragging: false, 76 | position, 77 | showAnimations, 78 | id: 'show-animations', 79 | }; 80 | 81 | // render 82 | return ( 83 |
91 | 94 | 102 |

{position}

103 | 104 | 105 | 106 |

107 | Toggle the checkbox to enable/disable piece movement animations 108 |

109 |
110 | ); 111 | }, 112 | }; 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ♟️ React Chessboard 2 | 3 |
4 | 5 | ![npm version](https://img.shields.io/npm/v/react-chessboard) 6 | ![npm downloads](https://img.shields.io/npm/dm/react-chessboard) 7 | ![license](https://img.shields.io/npm/l/react-chessboard) 8 | ![bundlesize](https://img.shields.io/bundlephobia/minzip/react-chessboard) 9 | 10 | A modern, responsive chessboard component for React applications. 11 | 12 | ![chessboard](./docs/assets/chessboard.png) 13 | 14 |
15 | 16 | ## ✨ Features 17 | 18 | - 🎯 Drag and drop 19 | - 🎨 Custom pieces 20 | - ♟️ Spare pieces 21 | - 🎭 Custom styling 22 | - ✨ Animation 23 | - 📐 Custom board dimensions 24 | - 🔄 Event handling 25 | - 📱 Mobile support 26 | - 📱 Responsive 27 | - ⌨️ Accessible 28 | - 🔷 TypeScript support 29 | - 🛠️ Helpful utility functions 30 | - ✨ And more! 31 | 32 | ## 📦 Installation 33 | 34 | ```bash 35 | pnpm add react-chessboard 36 | # or 37 | yarn add react-chessboard 38 | # or 39 | npm install react-chessboard 40 | ``` 41 | 42 | ## 🚀 Quick Start 43 | 44 | ```tsx 45 | import { Chessboard } from 'react-chessboard'; 46 | 47 | function App() { 48 | const chessboardOptions = { 49 | // your config options here 50 | }; 51 | 52 | return ; 53 | } 54 | ``` 55 | 56 | ## 📚 Documentation 57 | 58 | For detailed documentation, examples, and API reference, visit our documentation site: 59 | 60 | [📖 View Documentation](https://react-chessboard.vercel.app/) 61 | 62 | ## 🤝 Contributing 63 | 64 | Contributions are welcome! Please read our [contribution guide](https://react-chessboard.vercel.app/?path=/docs/developers-contributing-to-react-chessboard--docs) before submitting a Pull Request. 65 | 66 | Keen to contribute? Here is the current list of things we want to get done / are interested in adding if there is desire for it: 67 | 68 | ### Features 69 | 70 | - **Drag and Drop Enhancements** 71 | - Add `dropAnimation` prop to allow override of DragOverlay dropAnimation prop that is currently set to null. This will be for animating drag overlays back to their position on failed drops for example, instead of snapping back. 72 | - **Accessibility Improvements** 73 | - Review and enhance sensor implementations and accessibility. 74 | 75 | ### Documentation 76 | 77 | - **Framework Integrations** 78 | - Add framework specific documentation, Next.js, Vite, Remix 79 | - e.g. for Next.js, include `use client` directive at the top of the component consuming the Chessboard component. 80 | 81 | ### Infrastructure 82 | 83 | - **Testing** 84 | - Add test suite full of unit tests for utils and all options stories, and visual tests 85 | - **Storybook** 86 | - Upgrade to Storybook 9 87 | 88 | ## Join the community of developers 89 | 90 | Join the community of developers on the [Discord server](https://discord.gg/mTBuwNSNn5)! 91 | 92 | Whether you're: 93 | 94 | - building something cool with the component and want to show it off 95 | - struggling to implement something and need some help 96 | - have an idea for a new feature 97 | 98 | We'd love to have you join our growing community! 99 | 100 | ## 📄 License 101 | 102 | MIT © [Ryan Gregory](https://github.com/Clariity) 103 | -------------------------------------------------------------------------------- /docs/A_GetStarted.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | import { DocNavigation } from './components/DocNavigation'; 3 | 4 | 5 | 6 |
7 |

React Chessboard

8 |
9 | 10 |
17 | React Chessboard 22 |
23 | 24 | ## What is React Chessboard? 25 | 26 | React Chessboard is a React component that provides chessboard functionality to your web application. It is a simple and easy-to-use component allowing you to create a custom and flexible chessboard experience with a wide range of features. 27 | 28 | The component is purely a UI component and does not include any logic for the game of chess. The chess game logic should be independent to the component. This flexibility enables you to use the component in any project, regardless of the chess logic or chess variant you wish to build with. For example, you can use the [chess.js](https://www.npmjs.com/package/chess.js) library to handle the game logic. Example usages of the component with chess.js are shown in the [How to use](../?path=/docs/how-to-use-basic-examples--docs) documentation. 29 | 30 | ## Features 31 | 32 | The package includes the many helpful features, such as: 33 | 34 | - Drag and drop 35 | - Custom pieces 36 | - Spare pieces 37 | - Arrows 38 | - Custom styling 39 | - Animation 40 | - Custom board dimensions 41 | - Event handling 42 | - Mobile support 43 | - Responsive 44 | - Accessible 45 | - TypeScript support 46 | - Helpful utility functions 47 | - And more! 48 | 49 | ## Installation 50 | 51 | ```bash 52 | pnpm add react-chessboard 53 | ``` 54 | 55 | ```bash 56 | yarn add react-chessboard 57 | ``` 58 | 59 | ```bash 60 | npm i react-chessboard 61 | ``` 62 | 63 | ## Quick Start 64 | 65 | Here is a quick example of how to use the component. You will need to configure the component with the `options` prop. See the [Options API](../?path=/docs/how-to-use-options-api--docs) documentation for more information on the available options. You can also see the [Basic Examples](../?path=/docs/how-to-use-basic-examples--docs) documentation for more examples of how to use the component. 66 | 67 | ```tsx 68 | import { Chessboard } from 'react-chessboard'; 69 | 70 | function App() { 71 | const chessboardOptions = { 72 | // your config options here 73 | }; 74 | 75 | return ; 76 | } 77 | ``` 78 | 79 | ## Join the community of developers 80 | 81 | Join the community of developers on the [Discord server](https://discord.gg/mTBuwNSNn5). 82 | 83 | Whether you're: 84 | 85 | - building something cool with the component and want to show it off 86 | - struggling to implement something and need some help 87 | - have an idea for a new feature 88 | 89 | We'd love to have you join our growing community! 90 | 91 | ## Continue reading 92 | 93 | 101 | -------------------------------------------------------------------------------- /src/Piece.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { useChessboardContext } from './ChessboardProvider'; 4 | import type { DraggingPieceDataType, PieceDataType } from './types'; 5 | import { useEffect, useState } from 'react'; 6 | import { 7 | defaultDraggingPieceGhostStyle, 8 | defaultDraggingPieceStyle, 9 | } from './defaults'; 10 | 11 | type PieceProps = { 12 | clone?: boolean; 13 | isSparePiece?: DraggingPieceDataType['isSparePiece']; 14 | position: DraggingPieceDataType['position']; 15 | pieceType: PieceDataType['pieceType']; 16 | }; 17 | 18 | export const Piece = memo(function Piece({ 19 | clone, 20 | isSparePiece = false, 21 | position, 22 | pieceType, 23 | }: PieceProps) { 24 | const { 25 | id, 26 | allowDragging, 27 | animationDurationInMs, 28 | boardOrientation, 29 | canDragPiece, 30 | draggingPiece, 31 | draggingPieceStyle, 32 | draggingPieceGhostStyle, 33 | pieces, 34 | positionDifferences, 35 | onPieceClick, 36 | } = useChessboardContext(); 37 | 38 | const [animationStyle, setAnimationStyle] = useState({}); 39 | 40 | let cursorStyle = clone ? 'grabbing' : 'grab'; 41 | if ( 42 | !allowDragging || 43 | (canDragPiece && 44 | !canDragPiece({ piece: { pieceType }, isSparePiece, square: position })) 45 | ) { 46 | cursorStyle = 'pointer'; 47 | } 48 | 49 | useEffect(() => { 50 | if (positionDifferences[position]) { 51 | const sourceSquare = position; 52 | const targetSquare = positionDifferences[position]; 53 | 54 | const squareWidth = document 55 | .querySelector(`#${id}-square-${sourceSquare}`) 56 | ?.getBoundingClientRect().width; 57 | 58 | if (!squareWidth) { 59 | throw new Error('Square width not found'); 60 | } 61 | 62 | setAnimationStyle({ 63 | transform: `translate(${ 64 | (boardOrientation === 'black' ? -1 : 1) * 65 | (targetSquare.charCodeAt(0) - sourceSquare.charCodeAt(0)) * 66 | squareWidth 67 | }px, ${ 68 | (boardOrientation === 'black' ? -1 : 1) * 69 | (Number(sourceSquare[1]) - Number(targetSquare[1])) * 70 | squareWidth 71 | }px)`, 72 | transition: `transform ${animationDurationInMs}ms`, 73 | position: 'relative', // creates a new stacking context so the piece stays above squares during animation 74 | zIndex: 10, 75 | }); 76 | } else { 77 | setAnimationStyle({}); 78 | } 79 | }, [positionDifferences]); 80 | 81 | const PieceSvg = pieces[pieceType]; 82 | 83 | return ( 84 |
101 | onPieceClick?.({ isSparePiece, piece: { pieceType }, square: position }) 102 | } 103 | > 104 | 105 |
106 | ); 107 | }); 108 | -------------------------------------------------------------------------------- /docs/stories/options/ClearArrowsOnPositionChange.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/ClearArrowsOnPositionChange', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const ClearArrowsOnPositionChange: Story = { 16 | render: () => { 17 | const [clearArrowsOnPositionChange, setClearArrowsOnPositionChange] = 18 | useState(true); 19 | const [arrows] = useState([ 20 | { startSquare: 'e2', endSquare: 'e4', color: 'red' }, 21 | { startSquare: 'g1', endSquare: 'f3', color: 'blue' }, 22 | ]); 23 | const [position, setPosition] = useState( 24 | 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', 25 | ); 26 | 27 | // generate random FEN position 28 | function generateRandomFen() { 29 | const pieces = [ 30 | 'r', 31 | 'n', 32 | 'b', 33 | 'q', 34 | 'k', 35 | 'p', 36 | 'R', 37 | 'N', 38 | 'B', 39 | 'Q', 40 | 'K', 41 | 'P', 42 | ]; 43 | let fen = ''; 44 | 45 | // create 8 rows of random pieces 46 | for (let i = 0; i < 8; i++) { 47 | let emptyCount = 0; 48 | 49 | // create 8 columns of random pieces or empty squares 50 | for (let j = 0; j < 8; j++) { 51 | if (Math.random() < 0.2) { 52 | if (emptyCount > 0) { 53 | fen += emptyCount; 54 | emptyCount = 0; 55 | } 56 | 57 | fen += pieces[Math.floor(Math.random() * pieces.length)]; 58 | } else { 59 | emptyCount++; 60 | } 61 | } 62 | 63 | // add empty count to FEN string if there are empty squares 64 | if (emptyCount > 0) { 65 | fen += emptyCount; 66 | } 67 | 68 | // add slash between rows 69 | if (i < 7) { 70 | fen += '/'; 71 | } 72 | } 73 | 74 | // set the position 75 | setPosition(fen); 76 | } 77 | 78 | // chessboard options 79 | const chessboardOptions = { 80 | arrows, 81 | clearArrowsOnPositionChange, 82 | id: 'clear-arrows-on-click', 83 | position, 84 | }; 85 | 86 | // render 87 | return ( 88 |
96 | 104 | 105 | 108 | 109 | 110 | 111 |

112 | Toggle the checkbox to enable/disable clearing arrows when the 113 | position changes. 114 |

115 |
116 | ); 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /docs/stories/options/AnimationDurationInMs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess } from 'chess.js'; 3 | import { useState, useRef } from 'react'; 4 | 5 | import defaultMeta from '../basic-examples/Default.stories'; 6 | import { Chessboard } from '../../../src'; 7 | import type { PieceDropHandlerArgs } from '../../../src/types'; 8 | 9 | const meta: Meta = { 10 | ...defaultMeta, 11 | title: 'stories/Options/AnimationDurationInMs', 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const AnimationDurationInMs: Story = { 18 | render: () => { 19 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 20 | const chessGameRef = useRef(new Chess()); 21 | const chessGame = chessGameRef.current; 22 | 23 | // track the animation duration in state 24 | const [animationDuration, setAnimationDuration] = useState(300); 25 | 26 | // track the current position of the chess game in state to trigger a re-render of the chessboard 27 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 28 | 29 | // make a random "CPU" move 30 | function makeRandomMove() { 31 | // get all possible moves 32 | const possibleMoves = chessGame.moves(); 33 | 34 | // exit if the game is over 35 | if (chessGame.isGameOver()) { 36 | return; 37 | } 38 | 39 | // pick a random move 40 | const randomMove = 41 | possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; 42 | 43 | // make the move 44 | chessGame.move(randomMove); 45 | 46 | // update the position state 47 | setChessPosition(chessGame.fen()); 48 | } 49 | 50 | // handle piece drop 51 | const onPieceDrop = ({ 52 | sourceSquare, 53 | targetSquare, 54 | }: PieceDropHandlerArgs) => { 55 | if (!targetSquare) return false; 56 | 57 | try { 58 | chessGame.move({ 59 | from: sourceSquare, 60 | to: targetSquare, 61 | promotion: 'q', // always promote to a queen for example simplicity 62 | }); 63 | 64 | // update the position state 65 | setChessPosition(chessGame.fen()); 66 | 67 | // make random cpu move after a short delay 68 | setTimeout(makeRandomMove, 500); 69 | 70 | // return true as the move was successful 71 | return true; 72 | } catch { 73 | // return false as the move was not successful 74 | return false; 75 | } 76 | }; 77 | 78 | // chessboard options 79 | const chessboardOptions = { 80 | animationDurationInMs: animationDuration, 81 | position: chessPosition, 82 | onPieceDrop, 83 | id: 'animation-duration-in-ms', 84 | }; 85 | 86 | // render 87 | return ( 88 |
96 | 108 | 109 | 110 | 111 |

112 | Play against random moves. Try moving pieces to see the animation 113 | effects 114 |

115 |
116 | ); 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /docs/components/DocNavigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type NavigationItem = { 4 | href: string; 5 | title: string; 6 | description: string; 7 | }; 8 | 9 | type DocNavigationProps = { 10 | prev?: NavigationItem; 11 | next?: NavigationItem; 12 | }; 13 | 14 | type ArrowIconProps = { 15 | direction: 'left' | 'right'; 16 | }; 17 | 18 | function ArrowIcon({ direction }: ArrowIconProps) { 19 | return ( 20 | 30 | {direction === 'left' ? ( 31 | 32 | ) : ( 33 | 34 | )} 35 | 36 | ); 37 | } 38 | 39 | type NavigationCardProps = { 40 | item: NavigationItem; 41 | direction: 'left' | 'right'; 42 | isFullWidth: boolean; 43 | }; 44 | 45 | function NavigationCard({ item, direction, isFullWidth }: NavigationCardProps) { 46 | const cardStyle = { 47 | display: 'flex', 48 | alignItems: 'center', 49 | gap: '1rem', 50 | padding: '1.5rem', 51 | backgroundColor: 'white', 52 | borderRadius: '8px', 53 | boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', 54 | textDecoration: 'none', 55 | color: 'inherit', 56 | flex: isFullWidth ? '1 1 300px' : '0 0 auto', 57 | minWidth: '300px', 58 | maxWidth: isFullWidth ? 'none' : '400px', 59 | transition: 'transform 0.2s ease-in-out', 60 | }; 61 | 62 | const arrowContainerStyle = { 63 | display: 'flex', 64 | alignItems: 'center', 65 | justifyContent: 'center', 66 | width: '40px', 67 | flexShrink: 0, 68 | }; 69 | 70 | const contentStyle = { 71 | flex: '1 1 auto', 72 | minWidth: 0, // Allows text to shrink below its content size 73 | }; 74 | 75 | function handleMouseEnter(e: React.MouseEvent) { 76 | e.currentTarget.style.transform = 77 | direction === 'left' ? 'translateX(-4px)' : 'translateX(4px)'; 78 | } 79 | 80 | function handleMouseLeave(e: React.MouseEvent) { 81 | e.currentTarget.style.transform = 'translateX(0)'; 82 | } 83 | 84 | return ( 85 | 91 | {direction === 'left' && ( 92 |
93 | 94 |
95 | )} 96 | 97 |
98 |

99 | {item.title} 100 |

101 |

{item.description}

102 |
103 | 104 | {direction === 'right' && ( 105 |
106 | 107 |
108 | )} 109 |
110 | ); 111 | } 112 | 113 | export function DocNavigation({ prev, next }: DocNavigationProps) { 114 | const isFullWidth = Boolean(prev && next); 115 | 116 | return ( 117 |
128 | {prev && ( 129 | 134 | )} 135 | {next && ( 136 | 141 | )} 142 |
143 | ); 144 | } 145 | -------------------------------------------------------------------------------- /docs/stories/advanced-examples/Multiplayer.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess } from 'chess.js'; 3 | import { useState, useRef } from 'react'; 4 | 5 | import defaultMeta from '../basic-examples/Default.stories'; 6 | import { 7 | Chessboard, 8 | PieceDropHandlerArgs, 9 | PieceHandlerArgs, 10 | } from '../../../src'; 11 | 12 | const meta: Meta = { 13 | ...defaultMeta, 14 | title: 'stories/Multiplayer', 15 | decorators: [ 16 | (Story) => ( 17 |
23 | 24 |
25 | ), 26 | ], 27 | } satisfies Meta; 28 | 29 | export default meta; 30 | 31 | type Story = StoryObj; 32 | 33 | export const Multiplayer: Story = { 34 | render: () => { 35 | // create a chess game using a ref to maintain the game state across renders 36 | const chessGameRef = useRef(new Chess()); 37 | const chessGame = chessGameRef.current; 38 | 39 | // track the current position of the chess game in state 40 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 41 | 42 | // handle piece drop 43 | function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) { 44 | // type narrow targetSquare potentially being null (e.g. if dropped off board) 45 | if (!targetSquare) { 46 | return false; 47 | } 48 | 49 | // try to make the move according to chess.js logic 50 | try { 51 | chessGame.move({ 52 | from: sourceSquare, 53 | to: targetSquare, 54 | promotion: 'q', // always promote to a queen for example simplicity 55 | }); 56 | 57 | // update the position state upon successful move to trigger a re-render of the chessboard 58 | setChessPosition(chessGame.fen()); 59 | 60 | // return true as the move was successful 61 | return true; 62 | } catch { 63 | // return false as the move was not successful 64 | return false; 65 | } 66 | } 67 | 68 | // allow white to only drag white pieces 69 | function canDragPieceWhite({ piece }: PieceHandlerArgs) { 70 | return piece.pieceType[0] === 'w'; 71 | } 72 | 73 | // allow black to only drag black pieces 74 | function canDragPieceBlack({ piece }: PieceHandlerArgs) { 75 | return piece.pieceType[0] === 'b'; 76 | } 77 | 78 | // set the chessboard options for white's perspective 79 | const whiteBoardOptions = { 80 | canDragPiece: canDragPieceWhite, 81 | position: chessPosition, 82 | onPieceDrop, 83 | boardOrientation: 'white' as const, 84 | id: 'multiplayer-white', 85 | }; 86 | 87 | // set the chessboard options for black's perspective 88 | const blackBoardOptions = { 89 | canDragPiece: canDragPieceBlack, 90 | position: chessPosition, 91 | onPieceDrop, 92 | boardOrientation: 'black' as const, 93 | id: 'multiplayer-black', 94 | }; 95 | 96 | // render both chessboards side by side with a gap 97 | return ( 98 |
107 |
108 |

White's perspective

109 |
110 | 111 |
112 |
113 | 114 |
115 |

Black's perspective

116 |
117 | 118 |
119 |
120 |
121 | ); 122 | }, 123 | }; 124 | -------------------------------------------------------------------------------- /docs/stories/advanced-examples/3DBoard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useMemo } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/3DBoard', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | 14 | type Story = StoryObj; 15 | 16 | export const ThreeDBoard: Story = { 17 | render: () => { 18 | const threeDPieces = useMemo(() => { 19 | // define the pieces and their heights 20 | const pieces = [ 21 | { piece: 'wP', pieceHeight: 1 }, 22 | { piece: 'wN', pieceHeight: 1.2 }, 23 | { piece: 'wB', pieceHeight: 1.2 }, 24 | { piece: 'wR', pieceHeight: 1.2 }, 25 | { piece: 'wQ', pieceHeight: 1.5 }, 26 | { piece: 'wK', pieceHeight: 1.6 }, 27 | { piece: 'bP', pieceHeight: 1 }, 28 | { piece: 'bN', pieceHeight: 1.2 }, 29 | { piece: 'bB', pieceHeight: 1.2 }, 30 | { piece: 'bR', pieceHeight: 1.2 }, 31 | { piece: 'bQ', pieceHeight: 1.5 }, 32 | { piece: 'bK', pieceHeight: 1.6 }, 33 | ]; 34 | 35 | // get the width of a square to use for the piece sizes 36 | const squareWidth = 37 | document 38 | .querySelector(`[data-column="a"][data-row="1"]`) 39 | ?.getBoundingClientRect()?.width ?? 0; 40 | 41 | // create the piece components 42 | const pieceComponents: Record React.JSX.Element> = {}; 43 | pieces.forEach(({ piece, pieceHeight }) => { 44 | pieceComponents[piece] = () => ( 45 |
53 | 63 |
64 | ); 65 | }); 66 | return pieceComponents; 67 | }, []); 68 | 69 | // set the chessboard options 70 | const chessboardOptions = { 71 | id: '3d-board', 72 | boardStyle: { 73 | transform: 'rotateX(27.5deg)', 74 | transformOrigin: 'center', 75 | border: '16px solid #b8836f', 76 | borderStyle: 'outset', 77 | borderRightColor: ' #b27c67', 78 | borderRadius: '4px', 79 | boxShadow: 'rgba(0, 0, 0, 0.5) 2px 24px 24px 8px', 80 | borderRightWidth: '2px', 81 | borderLeftWidth: '2px', 82 | borderTopWidth: '0px', 83 | borderBottomWidth: '18px', 84 | borderTopLeftRadius: '8px', 85 | borderTopRightRadius: '8px', 86 | padding: '8px 8px 12px', 87 | background: '#e0c094', 88 | backgroundImage: 'url("wood-pattern.png")', 89 | backgroundSize: 'cover', 90 | overflow: 'visible', 91 | }, 92 | pieces: threeDPieces, 93 | lightSquareStyle: { 94 | backgroundColor: '#e0c094', 95 | backgroundImage: 'url("wood-pattern.png")', 96 | backgroundSize: 'cover', 97 | }, 98 | darkSquareStyle: { 99 | backgroundColor: '#865745', 100 | backgroundImage: 'url("wood-pattern.png")', 101 | backgroundSize: 'cover', 102 | }, 103 | }; 104 | 105 | // render the chessboard 106 | return ( 107 |
116 | 117 |
118 | ); 119 | }, 120 | }; 121 | -------------------------------------------------------------------------------- /docs/stories/options/OnArrowsChange.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | import type { Arrow } from '../../../src/types'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/Options/OnArrowsChange', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const OnArrowsChange: Story = { 17 | render: () => { 18 | const [internalArrows, setInternalArrows] = useState([]); 19 | const [arrowHistory, setArrowHistory] = useState([]); 20 | 21 | // handle arrows change 22 | const onArrowsChange = ({ arrows }: { arrows: Arrow[] }) => { 23 | setInternalArrows(arrows); 24 | setArrowHistory((prev) => [...prev, arrows]); 25 | }; 26 | 27 | // clear arrow history 28 | const clearHistory = () => { 29 | setArrowHistory([]); 30 | }; 31 | 32 | // chessboard options 33 | const chessboardOptions = { 34 | onArrowsChange, 35 | id: 'on-arrows-change', 36 | }; 37 | 38 | // render 39 | return ( 40 |
48 |
49 |

Current Internal Arrows:

50 |
51 | {internalArrows.length === 0 ? ( 52 | No arrows drawn 53 | ) : ( 54 | internalArrows.map((arrow, index) => ( 55 |
56 | {arrow.startSquare} → {arrow.endSquare} ({arrow.color}) 57 |
58 | )) 59 | )} 60 |
61 | 62 |

Arrow Change History:

63 |
70 | {arrowHistory.length === 0 ? ( 71 | No changes yet 72 | ) : ( 73 | arrowHistory.map((arrows, index) => ( 74 |
83 | Change {index + 1}: {arrows.length} arrow(s) 84 | {arrows.length > 0 && ( 85 |
86 | {arrows.map((arrow, arrowIndex) => ( 87 |
91 | {arrow.startSquare} → {arrow.endSquare} 92 |
93 | ))} 94 |
95 | )} 96 |
97 | )) 98 | )} 99 |
100 | 101 | 111 |
112 | 113 | 114 | 115 |

116 | Right-click and drag to draw arrows. The onArrowsChange callback will 117 | be triggered whenever internal arrows are added or removed. 118 |

119 |
120 | ); 121 | }, 122 | }; 123 | -------------------------------------------------------------------------------- /docs/stories/advanced-examples/MiniPuzzles.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { 6 | Chessboard, 7 | PieceDropHandlerArgs, 8 | PieceHandlerArgs, 9 | PositionDataType, 10 | } from '../../../src'; 11 | 12 | const meta: Meta = { 13 | ...defaultMeta, 14 | title: 'stories/MiniPuzzles', 15 | } satisfies Meta; 16 | 17 | export default meta; 18 | 19 | type Story = StoryObj; 20 | 21 | export const MiniPuzzles: Story = { 22 | render: () => { 23 | const [currentMoveIndex, setCurrentMoveIndex] = useState(0); 24 | const [position, setPosition] = useState({ 25 | a4: { pieceType: 'bR' }, 26 | c4: { pieceType: 'bK' }, 27 | e4: { pieceType: 'bN' }, 28 | d3: { pieceType: 'bP' }, 29 | f3: { pieceType: 'bQ' }, 30 | c2: { pieceType: 'wN' }, 31 | d2: { pieceType: 'wQ' }, 32 | b1: { pieceType: 'wN' }, 33 | } as PositionDataType); 34 | 35 | // as the squareStyles prop applies within a square instead of the whole square, we wouldn't be able to hide the squares with this prop 36 | // instead, we hide the squares by getting the square elements by their id and setting the display to none 37 | // "mini-puzzles" being the id we gave to the chessboard 38 | useEffect(() => { 39 | const e1 = document.getElementById('mini-puzzles-square-e1'); 40 | const f1 = document.getElementById('mini-puzzles-square-f1'); 41 | 42 | if (e1) { 43 | e1.style.display = 'none'; 44 | } 45 | if (f1) { 46 | f1.style.display = 'none'; 47 | } 48 | }, []); 49 | 50 | // moves for the puzzle 51 | const moves = [ 52 | { 53 | sourceSquare: 'd2', 54 | targetSquare: 'c3', 55 | }, 56 | { 57 | sourceSquare: 'e4', 58 | targetSquare: 'c3', 59 | }, 60 | { 61 | sourceSquare: 'b1', 62 | targetSquare: 'd2', 63 | }, 64 | ]; 65 | 66 | // handle piece drop 67 | function onPieceDrop({ 68 | sourceSquare, 69 | targetSquare, 70 | piece, 71 | }: PieceDropHandlerArgs) { 72 | const requiredMove = moves[currentMoveIndex]; 73 | 74 | // check if the move is valid 75 | if ( 76 | requiredMove.sourceSquare !== sourceSquare || 77 | requiredMove.targetSquare !== targetSquare 78 | ) { 79 | // return false as the move is not valid 80 | return false; 81 | } 82 | 83 | // update the position 84 | const newPosition = { ...position }; 85 | newPosition[targetSquare] = { 86 | pieceType: piece.pieceType, 87 | }; 88 | delete newPosition[sourceSquare]; 89 | setPosition(newPosition); 90 | 91 | // increment the current move index 92 | setCurrentMoveIndex((prev) => prev + 1); 93 | 94 | // define makeCpuMove inside onPieceDrop to capture current values 95 | const makeCpuMove = () => { 96 | const nextMoveIndex = currentMoveIndex + 1; 97 | 98 | // if there is another move, make it 99 | if (nextMoveIndex < moves.length) { 100 | const move = moves[nextMoveIndex]; 101 | const updatedPosition = { ...newPosition }; 102 | updatedPosition[move.targetSquare] = { 103 | pieceType: updatedPosition[move.sourceSquare].pieceType, 104 | }; 105 | delete updatedPosition[move.sourceSquare]; 106 | setPosition(updatedPosition); 107 | setCurrentMoveIndex(nextMoveIndex + 1); 108 | } 109 | }; 110 | 111 | // make the cpu move 112 | setTimeout(makeCpuMove, 200); 113 | 114 | // return true as the move was successful 115 | return true; 116 | } 117 | 118 | // only allow white pieces to be dragged 119 | function canDragPiece({ piece }: PieceHandlerArgs) { 120 | return piece.pieceType[0] === 'w'; 121 | } 122 | 123 | // set the chessboard options 124 | const chessboardOptions = { 125 | canDragPiece, 126 | onPieceDrop, 127 | chessboardRows: 4, 128 | chessboardColumns: 6, 129 | position, 130 | id: 'mini-puzzles', 131 | }; 132 | 133 | // render the chessboard 134 | return ( 135 |
143 |
144 | White to move, checkmate in 2 145 |
146 | 147 | 148 |
149 | ); 150 | }, 151 | }; 152 | -------------------------------------------------------------------------------- /docs/B_BasicExamples.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta } from '@storybook/blocks'; 2 | import { DocNavigation } from './components/DocNavigation'; 3 | import { HintMessage } from './components/HintMessage'; 4 | import { WarningMessage } from './components/WarningMessage'; 5 | 6 | import * as DefaultStories from './stories/basic-examples/Default.stories'; 7 | import * as PlayVsRandomStories from './stories/basic-examples/PlayVsRandom.stories'; 8 | import * as SparePiecesStories from './stories/basic-examples/SparePieces.stories'; 9 | import * as ClickToMoveStories from './stories/basic-examples/ClickToMove.stories'; 10 | import * as ClickOrDragToMoveStories from './stories/basic-examples/ClickOrDragToMove.stories'; 11 | 12 | 13 | 14 | # Basic examples 15 | 16 | These examples demonstrate basic and common use cases for React Chessboard. Each example includes explanations of key concepts and code snippets showing how to implement them. 17 | 18 | 19 | The code shown in the "Show code" dropdowns for each example does show the 20 | full code but it is not formatted in an ideal way and hides any helpful import 21 | statements. It is highly recommended to view the code in the [GitHub 22 | repository](https://github.com/Clariity/react-chessboard/tree/main/docs/stories/basic-examples) 23 | to see the code in a more readable format. 24 | 25 | 26 | ## Table of contents 27 | 28 | - [Default chessboard component](#default-chessboard-component) 29 | - [Using with chess.js](#using-with-chessjs) 30 | - [Spare pieces](#spare-pieces) 31 | - [Click to move](#click-to-move) 32 | - [Click or drag to move](#click-or-drag-to-move) 33 | 34 | ### Default chessboard component 35 | 36 | The default component is a simple chessboard with default pieces. It is unlikely that you will need to use this component in this state without any customisation or properties supplied. It is shown here for reference and as a starting point for your own customisation. 37 | 38 | 39 | 40 | ### Using with chess.js 41 | 42 | This example shows basic usage of the component with the chess.js library. It demonstrates how to handle piece drops and safely update the board position when a move is made. 43 | 44 | This utilises the `onPieceDrop` prop to handle the piece drop event and the `position` prop to update the board position when a move is made. 45 | 46 | A `chessGameRef` is used to prevent stale closures when making moves. Without it, functions like `onPieceDrop` would capture an outdated version of `chessGame` in their closure, leading to incorrect game state. The ref ensures we always access the latest game state, even in callbacks and timeouts. This scenario is quite specific to this example where there is a closure over old game states and the game state is updated in a timeout. 47 | 48 | 49 | 50 | ### Spare pieces 51 | 52 | This example shows how to use spare pieces with the chessboard component. Spare pieces are pieces that can be dragged onto the board from outside the main chessboard component. This is useful for setting up custom positions or creating chess puzzles. 53 | 54 | 55 | The spare pieces functionality requires wrapping your chessboard and spare 56 | pieces in the `ChessboardProvider` component. All props that would normally be 57 | passed to the `Chessboard` component must instead be passed to the 58 | `ChessboardProvider` via its `options` prop. 59 | 60 | 61 | When using spare pieces, you can drag pieces both onto and off the board. The `onPieceDrop` handler receives information about whether the piece was dropped on a valid square (via the `targetSquare` parameter) or off the board (`targetSquare` will be null). This allows you to implement custom logic for handling pieces being removed from the board. 62 | 63 | 64 | 65 | ### Click to move 66 | 67 | This example shows how to use the `onSquareClick` prop to handle square clicks. This is useful for implementing custom move logic, such as allowing the user to click on a piece to move it to a new square, instead of dragging the piece to the new square. 68 | 69 | 70 | 71 | ### Click or drag to move 72 | 73 | This example shows how to use the `onSquareClick` prop in tandem with the `onPieceDrop` prop to handle square click movement as well as drag movement. 74 | 75 | 76 | 77 | ## Continue reading 78 | 79 | 91 | -------------------------------------------------------------------------------- /docs/stories/basic-examples/SparePieces.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess, Color, PieceSymbol, Square } from 'chess.js'; 3 | import { useEffect, useRef, useState } from 'react'; 4 | 5 | import defaultMeta from './Default.stories'; 6 | import { 7 | Chessboard, 8 | ChessboardProvider, 9 | defaultPieces, 10 | PieceDropHandlerArgs, 11 | SparePiece, 12 | } from '../../../src'; 13 | 14 | const meta: Meta = { 15 | ...defaultMeta, 16 | title: 'stories/SparePieces', 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | 21 | type Story = StoryObj; 22 | 23 | export const SparePieces: Story = { 24 | render: () => { 25 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 26 | const chessGameRef = useRef( 27 | new Chess('8/8/8/8/8/8/8/8 w - - 0 1', { skipValidation: true }), 28 | ); 29 | const chessGame = chessGameRef.current; 30 | 31 | // track the current position of the chess game in state to trigger a re-render of the chessboard 32 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 33 | const [squareWidth, setSquareWidth] = useState(null); 34 | 35 | // get the width of a square to use for the spare piece sizes 36 | useEffect(() => { 37 | const square = document 38 | .querySelector(`[data-column="a"][data-row="1"]`) 39 | ?.getBoundingClientRect(); 40 | setSquareWidth(square?.width ?? null); 41 | }, []); 42 | 43 | // handle piece drop 44 | function onPieceDrop({ 45 | sourceSquare, 46 | targetSquare, 47 | piece, 48 | }: PieceDropHandlerArgs) { 49 | const color = piece.pieceType[0]; 50 | const type = piece.pieceType[1].toLowerCase(); 51 | 52 | // if the piece is dropped off the board, we need to remove it from the board 53 | if (!targetSquare) { 54 | chessGame.remove(sourceSquare as Square); 55 | setChessPosition(chessGame.fen()); 56 | 57 | // successful drop off board 58 | return true; 59 | } 60 | 61 | // if the piece is not a spare piece, we need to remove it from it's original square 62 | if (!piece.isSparePiece) { 63 | chessGame.remove(sourceSquare as Square); 64 | } 65 | 66 | // try to place the piece on the board 67 | const success = chessGame.put( 68 | { color: color as Color, type: type as PieceSymbol }, 69 | targetSquare as Square, 70 | ); 71 | 72 | // show error message if cannot place another king 73 | if (!success) { 74 | alert( 75 | `The board already contains a ${color === 'w' ? 'white' : 'black'} King piece`, 76 | ); 77 | return false; 78 | } 79 | 80 | // update the game state and return true if successful 81 | setChessPosition(chessGame.fen()); 82 | return true; 83 | } 84 | 85 | // get the piece types for the black and white spare pieces 86 | const blackPieceTypes: string[] = []; 87 | const whitePieceTypes: string[] = []; 88 | for (const pieceType of Object.keys(defaultPieces)) { 89 | if (pieceType[0] === 'b') { 90 | blackPieceTypes.push(pieceType as string); 91 | } else { 92 | whitePieceTypes.push(pieceType as string); 93 | } 94 | } 95 | 96 | // set the chessboard options 97 | const chessboardOptions = { 98 | position: chessPosition, 99 | onPieceDrop, 100 | id: 'spare-pieces', 101 | }; 102 | 103 | // render the chessboard and spare pieces 104 | return ( 105 | 106 | {squareWidth ? ( 107 |
115 | {blackPieceTypes.map((pieceType) => ( 116 |
123 | 124 |
125 | ))} 126 |
127 | ) : null} 128 | 129 | 130 | 131 | {squareWidth ? ( 132 |
140 | {whitePieceTypes.map((pieceType) => ( 141 |
148 | 149 |
150 | ))} 151 |
152 | ) : null} 153 |
154 | ); 155 | }, 156 | }; 157 | -------------------------------------------------------------------------------- /docs/stories/advanced-examples/AnalysisBoard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess, Square } from 'chess.js'; 3 | import { useEffect, useMemo, useRef, useState } from 'react'; 4 | 5 | import defaultMeta from '../basic-examples/Default.stories'; 6 | import { Chessboard, PieceDropHandlerArgs } from '../../../src'; 7 | import Engine from '../../stockfish/engine'; 8 | 9 | const meta: Meta = { 10 | ...defaultMeta, 11 | title: 'stories/AnalysisBoard', 12 | } satisfies Meta; 13 | 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | export const AnalysisBoard: Story = { 19 | render: () => { 20 | // initialise the engine 21 | const engine = useMemo(() => new Engine(), []); 22 | 23 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 24 | const chessGameRef = useRef(new Chess()); 25 | const chessGame = chessGameRef.current; 26 | 27 | // track the current position of the chess game in state to trigger a re-render of the chessboard 28 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 29 | 30 | // store engine variables 31 | const [positionEvaluation, setPositionEvaluation] = useState(0); 32 | const [depth, setDepth] = useState(10); 33 | const [bestLine, setBestLine] = useState(''); 34 | const [possibleMate, setPossibleMate] = useState(''); 35 | 36 | // when the chess game position changes, find the best move 37 | useEffect(() => { 38 | if (!(chessGame.isGameOver() || chessGame.isDraw())) { 39 | findBestMove(); 40 | } 41 | }, [chessGame.fen()]); 42 | 43 | // find the best move 44 | function findBestMove() { 45 | engine.evaluatePosition(chessGame.fen(), 18); 46 | engine.onMessage(({ positionEvaluation, possibleMate, pv, depth }) => { 47 | // ignore messages with a depth less than 10 48 | if (depth && depth < 10) { 49 | return; 50 | } 51 | 52 | // update the position evaluation 53 | if (positionEvaluation) { 54 | setPositionEvaluation( 55 | ((chessGame.turn() === 'w' ? 1 : -1) * Number(positionEvaluation)) / 56 | 100, 57 | ); 58 | } 59 | 60 | // update the possible mate, depth and best line 61 | if (possibleMate) { 62 | setPossibleMate(possibleMate); 63 | } 64 | if (depth) { 65 | setDepth(depth); 66 | } 67 | if (pv) { 68 | setBestLine(pv); 69 | } 70 | }); 71 | } 72 | 73 | // handle piece drop 74 | function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) { 75 | // type narrow targetSquare potentially being null (e.g. if dropped off board) 76 | if (!targetSquare) { 77 | return false; 78 | } 79 | 80 | // try to make the move 81 | try { 82 | chessGame.move({ 83 | from: sourceSquare, 84 | to: targetSquare, 85 | promotion: 'q', // always promote to a queen for example simplicity 86 | }); 87 | 88 | setPossibleMate(''); 89 | 90 | // update the game state 91 | setChessPosition(chessGame.fen()); 92 | 93 | // stop the engine (it will be restarted by the useEffect running findBestMove) 94 | engine.stop(); 95 | 96 | // reset the best line 97 | setBestLine(''); 98 | 99 | // if the game is over, return false 100 | if (chessGame.isGameOver() || chessGame.isDraw()) { 101 | return false; 102 | } 103 | 104 | // return true as the move was successful 105 | return true; 106 | } catch { 107 | // return false as the move was not successful 108 | return false; 109 | } 110 | } 111 | 112 | // get the best move 113 | const bestMove = bestLine?.split(' ')?.[0]; 114 | 115 | // set the chessboard options, using arrows to show the best move 116 | const chessboardOptions = { 117 | arrows: bestMove 118 | ? [ 119 | { 120 | startSquare: bestMove.substring(0, 2) as Square, 121 | endSquare: bestMove.substring(2, 4) as Square, 122 | color: 'rgb(0, 128, 0)', 123 | }, 124 | ] 125 | : undefined, 126 | position: chessPosition, 127 | onPieceDrop, 128 | id: 'analysis-board', 129 | }; 130 | 131 | // render the chessboard 132 | return ( 133 |
141 |
142 | Position Evaluation:{' '} 143 | {possibleMate ? `#${possibleMate}` : positionEvaluation} 144 | {'; '} 145 | Depth: {depth} 146 |
147 |
148 | Best line: {bestLine.slice(0, 40)} ... 149 |
150 | 151 | 152 | 153 |

154 | Make moves on the board to analyze positions. The green arrow shows 155 | Stockfish's suggested best move. The evaluation is shown in 156 | centipawns (positive numbers favor White, negative favor Black). 157 |

158 |
159 | ); 160 | }, 161 | }; 162 | -------------------------------------------------------------------------------- /docs/F_Contributing.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | 3 | 4 | 5 | # Contributing to react-chessboard 6 | 7 | Thank you for your interest in contributing to react-chessboard! Whether it's improving the documentation, adding new features, or fixing bugs, your contributions are welcome and appreciated. This document will guide you through the process of contributing to this project. 8 | 9 | ## GitHub contribution guide 10 | 11 | Before diving into the technical details, here are some general guidelines for contributing: 12 | 13 | - Check existing [issues](https://github.com/Clariity/react-chessboard/issues) and [pull requests](https://github.com/Clariity/react-chessboard/pulls) to avoid duplicating work 14 | - For major changes, open an issue first to discuss your proposed changes 15 | - Follow the code style and project guidelines outlined below 16 | - Include tests and documentation for new features 17 | - Keep pull requests focused on a single change 18 | - Be respectful and constructive in discussions 19 | 20 | Once you're ready to contribute, follow these steps: 21 | 22 | 1. Fork the [react-chessboard repository](https://github.com/Clariity/react-chessboard) 23 | 2. Clone your forked repository onto your development machine 24 | ```bash 25 | git clone https://github.com/YOUR-GITHUB-USERNAME/react-chessboard.git 26 | cd react-chessboard 27 | pnpm i 28 | ``` 29 | 3. Create a branch for your PR 30 | ```bash 31 | git checkout -b your-branch-name 32 | ``` 33 | 4. Set upstream remote 34 | ```bash 35 | git remote add upstream https://github.com/Clariity/react-chessboard.git 36 | ``` 37 | 5. Make your changes 38 | 6. Test your changes by running storybook 39 | ```bash 40 | pnpm storybook 41 | ``` 42 | 7. Push your changes 43 | ```bash 44 | git add . 45 | git commit -m "feat: cool new feature" 46 | git push --set-upstream origin your-branch-name 47 | ``` 48 | 8. Create pull request on GitHub 49 | 9. Contribute again 50 | ```bash 51 | git checkout main 52 | git pull upstream main 53 | git checkout -b your-new-branch-name 54 | ``` 55 | 56 | ## Commit Guidelines 57 | 58 | This project uses [Conventional Commits](https://www.conventionalcommits.org/) for commit messages and [semantic-release](https://semantic-release.gitbook.io/semantic-release/) for automated versioning, release notes, and publishing to npm. All commits are validated using [commitlint](https://commitlint.js.org/), so if your commit message does not follow the format, the commit will fail. 59 | 60 | ### Commit Message Format 61 | 62 | Each commit message should follow the conventional commit format: 63 | 64 | ``` 65 | (optional scope): 66 | 67 | [optional body] 68 | 69 | [optional footer] 70 | ``` 71 | 72 | Breaking changes should be indicated by a `!` in the type. e.g. `feat!` or `fix!`. 73 | 74 | ### Types 75 | 76 | - `feat`: A new feature (triggers minor version bump) 77 | - `fix`: A bug fix (triggers patch version bump) 78 | - `docs`: Documentation changes 79 | - `refactor`: Code changes that neither fix a bug nor add a feature 80 | - `test`: Adding or fixing tests 81 | - `chore`: Changes to the build process or auxiliary tools 82 | 83 | ### Examples 84 | 85 | ```bash 86 | # Feature commit 87 | git commit -m "feat: add support for custom board themes" 88 | 89 | # Bug fix commit 90 | git commit -m "fix: fix piece dragging on mobile devices" 91 | 92 | # Documentation commit 93 | git commit -m "docs: update installation instructions" 94 | 95 | # Breaking change commit 96 | git commit -m "feat!: change board orientation API" 97 | ``` 98 | 99 | ### Semantic Release 100 | 101 | The project uses semantic-release to automatically: 102 | 103 | - Determine the next version number based on commit types 104 | - Generate release notes 105 | - Publish to npm 106 | - Create git tags 107 | 108 | Version bumps are determined by commit types: 109 | 110 | - `feat` commits trigger a minor version bump (1.0.0 -> 1.1.0) 111 | - `fix` commits trigger a patch version bump (1.0.0 -> 1.0.1) 112 | - Commits with `feat!` or `fix!` in the type trigger a major version bump (1.1.2 -> 2.0.0) 113 | 114 | ## Project guidelines 115 | 116 | ### Dependencies 117 | 118 | The project aims to maintain a minimal dependency footprint to ensure optimal performance, smaller bundle size, and reduce potential security risks. Here are our dependency guidelines: 119 | 120 | - Keep dependencies to an absolute minimum 121 | - All dependencies must be actively maintained 122 | - Dependencies should have permissive licenses (MIT, Apache, etc.) 123 | - Evaluate each new dependency carefully for: 124 | - Bundle size impact 125 | - Security implications 126 | - Maintenance status 127 | - Browser compatibility 128 | 129 | #### Required Dependencies 130 | 131 | - `react` ^19.0.0 132 | - `react-dom` ^19.0.0 133 | 134 | #### Development Dependencies 135 | 136 | Development dependencies should also be kept minimal but include necessary tooling for: 137 | 138 | - Building (rollup, typescript) 139 | - Testing (storybook) 140 | - Code quality (eslint, prettier) 141 | - Release management (commitlint, semantic-release) 142 | 143 | ### Browser Support 144 | 145 | - Modern browsers (Chrome, Firefox, Safari, Edge) 146 | - No IE11 support required 147 | - Mobile browser support for touch events 148 | 149 | ### Accessibility 150 | 151 | - Use ARIA attributes for interactive elements 152 | - Ensure keyboard interactions are available 153 | - Provide clear feedback for user actions 154 | -------------------------------------------------------------------------------- /docs/stories/basic-examples/ClickToMove.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess, Square } from 'chess.js'; 3 | import { useState, useRef } from 'react'; 4 | 5 | import defaultMeta from './Default.stories'; 6 | import { Chessboard, SquareHandlerArgs } from '../../../src'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/ClickToMove', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const ClickToMove: Story = { 18 | render: () => { 19 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 20 | const chessGameRef = useRef(new Chess()); 21 | const chessGame = chessGameRef.current; 22 | 23 | // track the current position of the chess game in state to trigger a re-render of the chessboard 24 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 25 | const [moveFrom, setMoveFrom] = useState(''); 26 | const [optionSquares, setOptionSquares] = useState({}); 27 | 28 | // make a random "CPU" move 29 | function makeRandomMove() { 30 | // get all possible moves` 31 | const possibleMoves = chessGame.moves(); 32 | 33 | // exit if the game is over 34 | if (chessGame.isGameOver()) { 35 | return; 36 | } 37 | 38 | // pick a random move 39 | const randomMove = 40 | possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; 41 | 42 | // make the move 43 | chessGame.move(randomMove); 44 | 45 | // update the position state 46 | setChessPosition(chessGame.fen()); 47 | } 48 | 49 | // get the move options for a square to show valid moves 50 | function getMoveOptions(square: Square) { 51 | // get the moves for the square 52 | const moves = chessGame.moves({ 53 | square, 54 | verbose: true, 55 | }); 56 | 57 | // if no moves, clear the option squares 58 | if (moves.length === 0) { 59 | setOptionSquares({}); 60 | return false; 61 | } 62 | 63 | // create a new object to store the option squares 64 | const newSquares: Record = {}; 65 | 66 | // loop through the moves and set the option squares 67 | for (const move of moves) { 68 | newSquares[move.to] = { 69 | background: 70 | chessGame.get(move.to) && 71 | chessGame.get(move.to)?.color !== chessGame.get(square)?.color 72 | ? 'radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)' // larger circle for capturing 73 | : 'radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)', // smaller circle for moving 74 | borderRadius: '50%', 75 | }; 76 | } 77 | 78 | // set the square clicked to move from to yellow 79 | newSquares[square] = { 80 | background: 'rgba(255, 255, 0, 0.4)', 81 | }; 82 | 83 | // set the option squares 84 | setOptionSquares(newSquares); 85 | 86 | // return true to indicate that there are move options 87 | return true; 88 | } 89 | 90 | function onSquareClick({ square, piece }: SquareHandlerArgs) { 91 | // piece clicked to move 92 | if (!moveFrom && piece) { 93 | // get the move options for the square 94 | const hasMoveOptions = getMoveOptions(square as Square); 95 | 96 | // if move options, set the moveFrom to the square 97 | if (hasMoveOptions) { 98 | setMoveFrom(square); 99 | } 100 | 101 | // return early 102 | return; 103 | } 104 | 105 | // square clicked to move to, check if valid move 106 | const moves = chessGame.moves({ 107 | square: moveFrom as Square, 108 | verbose: true, 109 | }); 110 | const foundMove = moves.find( 111 | (m) => m.from === moveFrom && m.to === square, 112 | ); 113 | 114 | // not a valid move 115 | if (!foundMove) { 116 | // check if clicked on new piece 117 | const hasMoveOptions = getMoveOptions(square as Square); 118 | 119 | // if new piece, setMoveFrom, otherwise clear moveFrom 120 | setMoveFrom(hasMoveOptions ? square : ''); 121 | 122 | // return early 123 | return; 124 | } 125 | 126 | // is normal move 127 | try { 128 | chessGame.move({ 129 | from: moveFrom, 130 | to: square, 131 | promotion: 'q', 132 | }); 133 | } catch { 134 | // if invalid, setMoveFrom and getMoveOptions 135 | const hasMoveOptions = getMoveOptions(square as Square); 136 | 137 | // if new piece, setMoveFrom, otherwise clear moveFrom 138 | if (hasMoveOptions) { 139 | setMoveFrom(square); 140 | } 141 | 142 | // return early 143 | return; 144 | } 145 | 146 | // update the position state 147 | setChessPosition(chessGame.fen()); 148 | 149 | // make random cpu move after a short delay 150 | setTimeout(makeRandomMove, 300); 151 | 152 | // clear moveFrom and optionSquares 153 | setMoveFrom(''); 154 | setOptionSquares({}); 155 | } 156 | 157 | // set the chessboard options 158 | const chessboardOptions = { 159 | allowDragging: false, 160 | onSquareClick, 161 | position: chessPosition, 162 | squareStyles: optionSquares, 163 | id: 'click-to-move', 164 | }; 165 | 166 | // render the chessboard 167 | return ; 168 | }, 169 | }; 170 | -------------------------------------------------------------------------------- /docs/stories/advanced-examples/Premoves.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess } from 'chess.js'; 3 | import { useState, useRef } from 'react'; 4 | 5 | import defaultMeta from '../basic-examples/Default.stories'; 6 | import { 7 | Chessboard, 8 | fenStringToPositionObject, 9 | PieceDropHandlerArgs, 10 | PieceHandlerArgs, 11 | } from '../../../src'; 12 | 13 | const meta: Meta = { 14 | ...defaultMeta, 15 | title: 'stories/Premoves', 16 | } satisfies Meta; 17 | 18 | export default meta; 19 | 20 | type Story = StoryObj; 21 | 22 | export const Premoves: Story = { 23 | render: () => { 24 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 25 | const chessGameRef = useRef(new Chess()); 26 | const chessGame = chessGameRef.current; 27 | 28 | // track the current position of the chess game in state to trigger a re-render of the chessboard 29 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 30 | const [premoves, setPremoves] = useState([]); 31 | const [showAnimations, setShowAnimations] = useState(true); 32 | const premovesRef = useRef([]); 33 | 34 | // make a random "CPU" move 35 | function makeRandomMove() { 36 | // get all possible moves 37 | const possibleMoves = chessGame.moves(); 38 | 39 | // exit if the game is over 40 | if (chessGame.isGameOver()) { 41 | return; 42 | } 43 | 44 | // make a random move 45 | const randomMove = 46 | possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; 47 | chessGame.move(randomMove); 48 | setChessPosition(chessGame.fen()); 49 | 50 | // if there is a premove, remove it from the list and make it once animation is complete 51 | if (premovesRef.current.length > 0) { 52 | const nextPlayerPremove = premovesRef.current[0]; 53 | premovesRef.current.splice(0, 1); 54 | 55 | // wait for CPU move animation to complete 56 | setTimeout(() => { 57 | // execute the premove 58 | const premoveSuccessful = onPieceDrop(nextPlayerPremove); 59 | 60 | // if the premove was not successful, clear all premoves 61 | if (!premoveSuccessful) { 62 | premovesRef.current = []; 63 | } 64 | 65 | // update the premoves state 66 | setPremoves([...premovesRef.current]); 67 | 68 | // disable animations while clearing premoves 69 | setShowAnimations(false); 70 | 71 | // re-enable animations after a short delay 72 | setTimeout(() => { 73 | setShowAnimations(true); 74 | }, 50); 75 | }, 300); 76 | } 77 | } 78 | 79 | // handle piece drop 80 | function onPieceDrop({ 81 | sourceSquare, 82 | targetSquare, 83 | piece, 84 | }: PieceDropHandlerArgs) { 85 | // type narrow targetSquare potentially being null (e.g. if dropped off board) or user dropping piece onto same square 86 | if (!targetSquare || sourceSquare === targetSquare) { 87 | return false; 88 | } 89 | 90 | // check if a premove (piece isn't the color of the current player's turn) 91 | const pieceColor = piece.pieceType[0]; // 'w' or 'b' 92 | if (chessGame.turn() !== pieceColor) { 93 | premovesRef.current.push({ sourceSquare, targetSquare, piece }); 94 | setPremoves([...premovesRef.current]); 95 | // return early to stop processing the move and return true to not animate the move 96 | return true; 97 | } 98 | 99 | // try to make the move 100 | try { 101 | chessGame.move({ 102 | from: sourceSquare, 103 | to: targetSquare, 104 | promotion: 'q', // always promote to a queen for example simplicity 105 | }); 106 | 107 | // update the position state 108 | setChessPosition(chessGame.fen()); 109 | 110 | // make random cpu move after a slightly longer delay to allow user to premove 111 | setTimeout(makeRandomMove, 3000); 112 | 113 | // return true as the move was successful 114 | return true; 115 | } catch { 116 | // return false as the move was not successful 117 | return false; 118 | } 119 | } 120 | 121 | // clear all premoves on right click 122 | function onSquareRightClick() { 123 | premovesRef.current = []; 124 | setPremoves([...premovesRef.current]); 125 | 126 | // disable animations while clearing premoves 127 | setShowAnimations(false); 128 | 129 | // re-enable animations after a short delay 130 | setTimeout(() => { 131 | setShowAnimations(true); 132 | }, 50); 133 | } 134 | 135 | // only allow white pieces to be dragged 136 | function canDragPiece({ piece }: PieceHandlerArgs) { 137 | return piece.pieceType[0] === 'w'; 138 | } 139 | 140 | // create a position object from the fen string to split the premoves from the game state 141 | const position = fenStringToPositionObject(chessPosition, 8, 8); 142 | const squareStyles: Record = {}; 143 | 144 | // add premoves to the position object to show them on the board 145 | for (const premove of premoves) { 146 | delete position[premove.sourceSquare]; 147 | position[premove.targetSquare!] = { 148 | pieceType: premove.piece.pieceType, 149 | }; 150 | squareStyles[premove.sourceSquare] = { 151 | backgroundColor: 'rgba(255,0,0,0.2)', 152 | }; 153 | squareStyles[premove.targetSquare!] = { 154 | backgroundColor: 'rgba(255,0,0,0.2)', 155 | }; 156 | } 157 | 158 | // set the chessboard options 159 | const chessboardOptions = { 160 | canDragPiece, 161 | onPieceDrop, 162 | onSquareRightClick, 163 | position, 164 | showAnimations, 165 | squareStyles, 166 | id: 'premoves', 167 | }; 168 | 169 | // render the chessboard 170 | return ; 171 | }, 172 | }; 173 | -------------------------------------------------------------------------------- /docs/stories/basic-examples/ClickOrDragToMove.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess, Square } from 'chess.js'; 3 | import { useState, useRef } from 'react'; 4 | 5 | import defaultMeta from './Default.stories'; 6 | import { 7 | Chessboard, 8 | SquareHandlerArgs, 9 | PieceDropHandlerArgs, 10 | } from '../../../src'; 11 | 12 | const meta: Meta = { 13 | ...defaultMeta, 14 | title: 'stories/ClickOrDragToMove', 15 | } satisfies Meta; 16 | 17 | export default meta; 18 | 19 | type Story = StoryObj; 20 | 21 | export const ClickOrDragToMove: Story = { 22 | render: () => { 23 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 24 | const chessGameRef = useRef(new Chess()); 25 | const chessGame = chessGameRef.current; 26 | 27 | // track the current position of the chess game in state to trigger a re-render of the chessboard 28 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 29 | const [moveFrom, setMoveFrom] = useState(''); 30 | const [optionSquares, setOptionSquares] = useState({}); 31 | 32 | // make a random "CPU" move 33 | function makeRandomMove() { 34 | // get all possible moves` 35 | const possibleMoves = chessGame.moves(); 36 | 37 | // exit if the game is over 38 | if (chessGame.isGameOver()) { 39 | return; 40 | } 41 | 42 | // pick a random move 43 | const randomMove = 44 | possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; 45 | 46 | // make the move 47 | chessGame.move(randomMove); 48 | 49 | // update the position state 50 | setChessPosition(chessGame.fen()); 51 | } 52 | 53 | // get the move options for a square to show valid moves 54 | function getMoveOptions(square: Square) { 55 | // get the moves for the square 56 | const moves = chessGame.moves({ 57 | square, 58 | verbose: true, 59 | }); 60 | 61 | // if no moves, clear the option squares 62 | if (moves.length === 0) { 63 | setOptionSquares({}); 64 | return false; 65 | } 66 | 67 | // create a new object to store the option squares 68 | const newSquares: Record = {}; 69 | 70 | // loop through the moves and set the option squares 71 | for (const move of moves) { 72 | newSquares[move.to] = { 73 | background: 74 | chessGame.get(move.to) && 75 | chessGame.get(move.to)?.color !== chessGame.get(square)?.color 76 | ? 'radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)' // larger circle for capturing 77 | : 'radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)', // smaller circle for moving 78 | borderRadius: '50%', 79 | }; 80 | } 81 | 82 | // set the square clicked to move from to yellow 83 | newSquares[square] = { 84 | background: 'rgba(255, 255, 0, 0.4)', 85 | }; 86 | 87 | // set the option squares 88 | setOptionSquares(newSquares); 89 | 90 | // return true to indicate that there are move options 91 | return true; 92 | } 93 | 94 | function onSquareClick({ square, piece }: SquareHandlerArgs) { 95 | // piece clicked to move 96 | if (!moveFrom && piece) { 97 | // get the move options for the square 98 | const hasMoveOptions = getMoveOptions(square as Square); 99 | 100 | // if move options, set the moveFrom to the square 101 | if (hasMoveOptions) { 102 | setMoveFrom(square); 103 | } 104 | 105 | // return early 106 | return; 107 | } 108 | 109 | // square clicked to move to, check if valid move 110 | const moves = chessGame.moves({ 111 | square: moveFrom as Square, 112 | verbose: true, 113 | }); 114 | const foundMove = moves.find( 115 | (m) => m.from === moveFrom && m.to === square, 116 | ); 117 | 118 | // not a valid move 119 | if (!foundMove) { 120 | // check if clicked on new piece 121 | const hasMoveOptions = getMoveOptions(square as Square); 122 | 123 | // if new piece, setMoveFrom, otherwise clear moveFrom 124 | setMoveFrom(hasMoveOptions ? square : ''); 125 | 126 | // return early 127 | return; 128 | } 129 | 130 | // is normal move 131 | try { 132 | chessGame.move({ 133 | from: moveFrom, 134 | to: square, 135 | promotion: 'q', 136 | }); 137 | } catch { 138 | // if invalid, setMoveFrom and getMoveOptions 139 | const hasMoveOptions = getMoveOptions(square as Square); 140 | 141 | // if new piece, setMoveFrom, otherwise clear moveFrom 142 | if (hasMoveOptions) { 143 | setMoveFrom(square); 144 | } 145 | 146 | // return early 147 | return; 148 | } 149 | 150 | // update the position state 151 | setChessPosition(chessGame.fen()); 152 | 153 | // make random cpu move after a short delay 154 | setTimeout(makeRandomMove, 300); 155 | 156 | // clear moveFrom and optionSquares 157 | setMoveFrom(''); 158 | setOptionSquares({}); 159 | } 160 | 161 | // handle piece drop 162 | function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) { 163 | // type narrow targetSquare potentially being null (e.g. if dropped off board) 164 | if (!targetSquare) { 165 | return false; 166 | } 167 | 168 | // try to make the move according to chess.js logic 169 | try { 170 | chessGame.move({ 171 | from: sourceSquare, 172 | to: targetSquare, 173 | promotion: 'q', // always promote to a queen for example simplicity 174 | }); 175 | 176 | // update the position state upon successful move to trigger a re-render of the chessboard 177 | setChessPosition(chessGame.fen()); 178 | 179 | // clear moveFrom and optionSquares 180 | setMoveFrom(''); 181 | setOptionSquares({}); 182 | 183 | // make random cpu move after a short delay 184 | setTimeout(makeRandomMove, 500); 185 | 186 | // return true as the move was successful 187 | return true; 188 | } catch { 189 | // return false as the move was not successful 190 | return false; 191 | } 192 | } 193 | 194 | // set the chessboard options 195 | const chessboardOptions = { 196 | onPieceDrop, 197 | onSquareClick, 198 | position: chessPosition, 199 | squareStyles: optionSquares, 200 | id: 'click-or-drag-to-move', 201 | }; 202 | 203 | // render the chessboard 204 | return ; 205 | }, 206 | }; 207 | -------------------------------------------------------------------------------- /src/Arrows.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | 3 | import { useChessboardContext } from './ChessboardProvider'; 4 | import { getRelativeCoords } from './utils'; 5 | 6 | export function Arrows() { 7 | const { 8 | id, 9 | arrows, 10 | arrowOptions, 11 | boardOrientation, 12 | chessboardColumns, 13 | chessboardRows, 14 | internalArrows, 15 | newArrowStartSquare, 16 | newArrowOverSquare, 17 | } = useChessboardContext(); 18 | 19 | const viewBoxWidth = 2048; 20 | const viewBoxHeight = viewBoxWidth * (chessboardRows / chessboardColumns); 21 | 22 | const currentlyDrawingArrow = 23 | newArrowStartSquare && 24 | newArrowOverSquare && 25 | newArrowStartSquare !== newArrowOverSquare.square 26 | ? { 27 | startSquare: newArrowStartSquare, 28 | endSquare: newArrowOverSquare.square, 29 | color: newArrowOverSquare.color, 30 | } 31 | : null; 32 | 33 | const arrowsToDraw = currentlyDrawingArrow 34 | ? [...arrows, ...internalArrows, currentlyDrawingArrow] 35 | : [...arrows, ...internalArrows]; 36 | 37 | return ( 38 | 50 | {arrowsToDraw.map((arrow, i) => { 51 | const from = getRelativeCoords( 52 | boardOrientation, 53 | viewBoxWidth, 54 | chessboardColumns, 55 | chessboardRows, 56 | arrow.startSquare, 57 | ); 58 | const to = getRelativeCoords( 59 | boardOrientation, 60 | viewBoxWidth, 61 | chessboardColumns, 62 | chessboardRows, 63 | arrow.endSquare, 64 | ); 65 | 66 | // we want to shorten the arrow length so the tip of the arrow is more central to the target square instead of running over the center 67 | const squareWidth = viewBoxWidth / chessboardColumns; 68 | let ARROW_LENGTH_REDUCER = 69 | squareWidth / arrowOptions.arrowLengthReducerDenominator; 70 | 71 | const isArrowActive = 72 | currentlyDrawingArrow && i === arrowsToDraw.length - 1; 73 | 74 | // if there are different arrows targeting the same square make their length a bit shorter 75 | if ( 76 | arrowsToDraw.some( 77 | (restArrow) => 78 | restArrow.startSquare !== arrow.startSquare && 79 | restArrow.endSquare === arrow.endSquare, 80 | ) && 81 | !isArrowActive 82 | ) { 83 | ARROW_LENGTH_REDUCER = 84 | squareWidth / arrowOptions.sameTargetArrowLengthReducerDenominator; 85 | } 86 | 87 | // Calculate the difference in x and y coordinates between start and end points 88 | const dx = to.x - from.x; 89 | const dy = to.y - from.y; 90 | 91 | // Calculate the total distance between points using Pythagorean theorem 92 | // This gives us the length of the arrow if it went from center to center 93 | const r = Math.hypot(dy, dx); 94 | 95 | let pathD: string; 96 | 97 | // Is Knight move 98 | if (r === Math.hypot(1, 2) * squareWidth) { 99 | // The mid point is only used in Knight move drawing 100 | // and here we prioritise drawing along the long edge 101 | // by defining the midpoint depending on which is bigger X or Y 102 | const mid = 103 | Math.abs(dx) < Math.abs(dy) 104 | ? { 105 | x: from.x, 106 | y: to.y, 107 | } 108 | : { 109 | x: to.x, 110 | y: from.y, 111 | }; 112 | 113 | // Calculate the difference in x and y coordinates between mid and end points 114 | const dxEnd = to.x - mid.x; 115 | const dyEnd = to.y - mid.y; 116 | 117 | // End arrow distance is always one squareWidth for Knight moves 118 | const rEnd = squareWidth; 119 | 120 | // Calculate the new end point for the arrow 121 | // We subtract ARROW_LENGTH_REDUCER from the end line distance to make the arrow 122 | // stop before reaching the center of the target square 123 | const end = { 124 | // Calculate new end x coordinate by: 125 | // 1. Taking the mid->end x direction (dxEnd) 126 | // 2. Scaling it by (rEnd - ARROW_LENGTH_REDUCER) / rEnd to shorten it 127 | // 3. Adding to the mid x coordinate 128 | x: mid.x + (dxEnd * (rEnd - ARROW_LENGTH_REDUCER)) / rEnd, 129 | // Same calculation for y coordinate 130 | y: mid.y + (dyEnd * (rEnd - ARROW_LENGTH_REDUCER)) / rEnd, 131 | }; 132 | 133 | pathD = `M${from.x},${from.y} L${mid.x},${mid.y} L${end.x},${end.y}`; 134 | } else { 135 | // Calculate the new end point for the arrow 136 | // We subtract ARROW_LENGTH_REDUCER from the total distance to make the arrow 137 | // stop before reaching the center of the target square 138 | const end = { 139 | // Calculate new end x coordinate by: 140 | // 1. Taking the original x direction (dx) 141 | // 2. Scaling it by (r - ARROW_LENGTH_REDUCER) / r to shorten it 142 | // 3. Adding to the starting x coordinate 143 | x: from.x + (dx * (r - ARROW_LENGTH_REDUCER)) / r, 144 | // Same calculation for y coordinate 145 | y: from.y + (dy * (r - ARROW_LENGTH_REDUCER)) / r, 146 | }; 147 | 148 | pathD = `M${from.x},${from.y} L${end.x},${end.y}`; 149 | } 150 | 151 | return ( 152 | 157 | 165 | 166 | 167 | 184 | 185 | ); 186 | })} 187 | 188 | ); 189 | } 190 | -------------------------------------------------------------------------------- /docs/C_AdvancedExamples.mdx: -------------------------------------------------------------------------------- 1 | import { Canvas, Meta } from '@storybook/blocks'; 2 | import { DocNavigation } from './components/DocNavigation'; 3 | import { HintMessage } from './components/HintMessage'; 4 | 5 | import * as AnalysisBoardStories from './stories/advanced-examples/AnalysisBoard.stories'; 6 | import * as MiniPuzzlesStories from './stories/advanced-examples/MiniPuzzles.stories'; 7 | import * as MultiplayerStories from './stories/advanced-examples/Multiplayer.stories'; 8 | import * as PremovesStories from './stories/advanced-examples/Premoves.stories'; 9 | import * as PiecePromotionStories from './stories/advanced-examples/PiecePromotion.stories'; 10 | import * as FourPlayerChessStories from './stories/advanced-examples/FourPlayerChess.stories'; 11 | import * as ThreeDBoardStories from './stories/advanced-examples/3DBoard.stories'; 12 | 13 | 14 | 15 | # Advanced examples 16 | 17 | These examples demonstrate more complex use cases for React Chessboard. Each example includes explanations of key concepts and code snippets showing how to implement them. 18 | 19 | 20 | The code shown in the "Show code" dropdowns for each example does show the 21 | full code where possible, but it is not formatted in an ideal way and hides 22 | any helpful import statements. It is highly recommended to view the code in 23 | the [GitHub 24 | repository](https://github.com/Clariity/react-chessboard/tree/main/docs/stories/advanced-examples) 25 | to see the code in a more readable format. 26 | 27 | 28 | ## Table of contents 29 | 30 | - [Analysis board](#analysis-board) 31 | - [Mini puzzles](#mini-puzzles) 32 | - [Multiplayer](#multiplayer) 33 | - [Premoves](#premoves) 34 | - [Promotion piece selection](#promotion-piece-selection) 35 | - [Four player chess](#four-player-chess) 36 | - [3D board](#3d-board) 37 | 38 | ### Analysis board 39 | 40 | This example shows you how to implement an analysis board with the component. The analysis board is a board that shows a chess engine's evaluation of the current position and the best move it thinks can be played. 41 | 42 | The engine used in this example is [Stockfish](https://stockfishchess.org/), which is a free and open-source chess engine. In this example we are using the [stockfish.wasm](https://github.com/lichess-org/stockfish.wasm) library by Lichess, which is a WebAssembly build of Stockfish. 43 | 44 | To implement the analysis board, there are a few key requirements: 45 | 46 | 1. **Web Worker Implementation**: The chess engine runs in a web worker to prevent blocking the main thread. This ensures the UI remains responsive while the engine is calculating moves. You can learn more about web workers in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). 47 | 48 | 2. **Required Files**: You'll need to include two additional files in your project: 49 | 50 | - `stockfish.wasm.js`: The JavaScript wrapper for the WebAssembly engine 51 | - `stockfish.wasm`: The actual WebAssembly binary of the Stockfish engine 52 | 53 | These files should be placed in your public directory so they can be accessed by the web worker. You can download these files from the [react-chessboard repository](https://github.com/Clariity/react-chessboard/tree/main/docs/stockfish): 54 | 55 | - [stockfish.wasm.js](https://github.com/Clariity/react-chessboard/blob/main/docs/stockfish/stockfish.wasm.js) 56 | - [stockfish.wasm](https://github.com/Clariity/react-chessboard/blob/main/docs/stockfish/stockfish.wasm) 57 | 58 | 3. **Engine Helper Class**: The example uses a helper class called `Engine` to simplify interaction with the Stockfish engine. This class: 59 | 60 | - Initializes the web worker 61 | - Handles communication with the engine 62 | - Provides methods for position evaluation and move calculation 63 | - Manages the engine's lifecycle 64 | 65 | You can find the complete implementation of the Engine class in the [react-chessboard repository](https://github.com/Clariity/react-chessboard/blob/main/docs/stockfish/engine.ts). 66 | 67 | 68 | 69 | ### Mini puzzles 70 | 71 | This example shows you how to implement a mini puzzle with the component. This example is inspired from the mate in two puzzles in the [Pocket Chess](https://play.google.com/store/apps/details?id=com.dkxqzbfkjt.pocketchess&hl=en_GB&pli=1) app. It highlights the ability to create non-standard boards with logic following predefined moves. 72 | 73 | 74 | 75 | ### Multiplayer 76 | 77 | This example demonstrates how to implement a multiplayer chess experience with the component. This aims to highlight a centralised game state where each player can see the board from their own perspective. 78 | 79 | In examples where the game is played over a network, the player should have a local game state and a remote game state. The local game state is the game state that the player sees on their own board, and the remote game state is the game state that the player sees on the opponent's board. These should be kept in sync by sending the game state to the opponent over the network. In this scenario, the local player would immediately see their board update, and the remote player would receive that update with a delay equal to the time it takes to send the game state to the opponent over the network. 80 | 81 | 82 | 83 | ### Premoves 84 | 85 | This example shows you how can you implement premoves with the component. Premoves are when you make a move and then before your opponent makes their move, you make a move to be played automatically after your opponent's move. 86 | 87 | 88 | 89 | ### Promotion piece selection 90 | 91 | This example shows you how to implement promotion piece selection with the component by using the `onPieceDrop` prop to capture the promotion move, show a dialog to select the piece that the pawn will be promoted to, and then update the board `position` prop to update the board position to the promotion move. 92 | 93 | 94 | 95 | ### Four player chess 96 | 97 | This example shows you how to implement a four player chess game with the component. This example is inspired from the [Four Player Chess](https://www.chess.com/variants/4-player-chess) variant on Chess.com. It highlights the ability to create a non-standard board with multiple orientations and piece colours. 98 | 99 | 100 | 101 | ### 3D board 102 | 103 | This example shows you how to implement a 3D chessboard with the component by using the `boardStyle` prop to create a 3D board and the `pieces` prop to create 3D pieces with images. 104 | 105 | 106 | 107 | ## Continue reading 108 | 109 | 122 | -------------------------------------------------------------------------------- /docs/stories/options/ArrowOptions.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard } from '../../../src'; 6 | 7 | const meta: Meta = { 8 | ...defaultMeta, 9 | title: 'stories/Options/ArrowOptions', 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const ArrowOptions: Story = { 16 | render: () => { 17 | // default arrow settings 18 | const defaultArrowOptions = { 19 | color: '#ffaa00', 20 | secondaryColor: '#ffaa00', 21 | tertiaryColor: '#000000', 22 | arrowLengthReducerDenominator: 8, 23 | sameTargetArrowLengthReducerDenominator: 4, 24 | arrowWidthDenominator: 5, 25 | activeArrowWidthMultiplier: 0.9, 26 | opacity: 0.65, 27 | activeOpacity: 0.5, 28 | }; 29 | 30 | // arrows 31 | const arrows = [ 32 | { startSquare: 'e2', endSquare: 'e4', color: '#ffaa00' }, 33 | { startSquare: 'g1', endSquare: 'f3', color: '#ffaa00' }, 34 | { startSquare: 'd2', endSquare: 'd4', color: '#ffaa00' }, 35 | { startSquare: 'b1', endSquare: 'c3', color: '#ffaa00' }, 36 | { startSquare: 'f1', endSquare: 'b5', color: '#ffaa00' }, 37 | ]; 38 | 39 | // arrow settings 40 | const [arrowOptions, setarrowOptions] = useState(defaultArrowOptions); 41 | 42 | // chessboard options 43 | const chessboardOptions = { 44 | arrows, 45 | arrowOptions, 46 | id: 'arrow-options', 47 | }; 48 | 49 | // render 50 | return ( 51 |
59 |
68 | {/* Colors */} 69 |
70 | 71 | 75 | setarrowOptions({ ...arrowOptions, color: e.target.value }) 76 | } 77 | /> 78 |
79 |
80 | 81 | 85 | setarrowOptions({ 86 | ...arrowOptions, 87 | secondaryColor: e.target.value, 88 | }) 89 | } 90 | /> 91 |
92 |
93 | 94 | 98 | setarrowOptions({ 99 | ...arrowOptions, 100 | tertiaryColor: e.target.value, 101 | }) 102 | } 103 | /> 104 |
105 | 106 | {/* Lengths */} 107 |
108 | 111 | 117 | setarrowOptions({ 118 | ...arrowOptions, 119 | arrowLengthReducerDenominator: Number(e.target.value), 120 | }) 121 | } 122 | /> 123 |
124 |
125 | 129 | 135 | setarrowOptions({ 136 | ...arrowOptions, 137 | sameTargetArrowLengthReducerDenominator: Number( 138 | e.target.value, 139 | ), 140 | }) 141 | } 142 | /> 143 |
144 |
145 | 146 | 152 | setarrowOptions({ 153 | ...arrowOptions, 154 | arrowWidthDenominator: Number(e.target.value), 155 | }) 156 | } 157 | /> 158 |
159 | 160 | {/* Opacity and Active Settings */} 161 |
162 | 163 | 170 | setarrowOptions({ 171 | ...arrowOptions, 172 | opacity: Number(e.target.value), 173 | }) 174 | } 175 | /> 176 |
177 |
178 | 179 | 186 | setarrowOptions({ 187 | ...arrowOptions, 188 | activeOpacity: Number(e.target.value), 189 | }) 190 | } 191 | /> 192 |
193 |
194 | 198 | 205 | setarrowOptions({ 206 | ...arrowOptions, 207 | activeArrowWidthMultiplier: Number(e.target.value), 208 | }) 209 | } 210 | /> 211 |
212 |
213 | 214 | 217 | 218 | 219 | 220 |

221 | Adjust the controls above to customize arrow appearance. Click the 222 | button to reset to default settings. 223 |

224 |
225 | ); 226 | }, 227 | }; 228 | -------------------------------------------------------------------------------- /docs/stories/advanced-examples/PiecePromotion.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Chess, Square, PieceSymbol } from 'chess.js'; 3 | import { useRef, useState } from 'react'; 4 | 5 | import defaultMeta from '../basic-examples/Default.stories'; 6 | import { 7 | Chessboard, 8 | chessColumnToColumnIndex, 9 | defaultPieces, 10 | PieceDropHandlerArgs, 11 | PieceRenderObject, 12 | } from '../../../src'; 13 | 14 | const meta: Meta = { 15 | ...defaultMeta, 16 | title: 'stories/PiecePromotion', 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | 21 | type Story = StoryObj; 22 | 23 | export const PiecePromotion: Story = { 24 | render: () => { 25 | // create a chess game using a ref to always have access to the latest game state within closures and maintain the game state across renders 26 | const chessGameRef = useRef(new Chess('8/P7/7K/8/8/8/8/k7 w - - 0 1')); 27 | const chessGame = chessGameRef.current; 28 | 29 | // track the current position of the chess game in state to trigger a re-render of the chessboard 30 | const [chessPosition, setChessPosition] = useState(chessGame.fen()); 31 | 32 | // track the promotion move 33 | const [promotionMove, setPromotionMove] = useState | null>(null); 37 | 38 | // handle piece drop 39 | function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) { 40 | // type narrow targetSquare potentially being null (e.g. if dropped off board) 41 | if (!targetSquare) { 42 | return false; 43 | } 44 | 45 | // target square is a promotion square, check if valid and show promotion dialog 46 | if (targetSquare.match(/\d+$/)?.[0] === '8') { 47 | // get all possible moves for the source square 48 | const possibleMoves = chessGame.moves({ 49 | square: sourceSquare as Square, 50 | }); 51 | 52 | // check if target square is in possible moves (accounting for promotion notation) 53 | if (possibleMoves.some((move) => move.startsWith(`${targetSquare}=`))) { 54 | setPromotionMove({ 55 | sourceSquare, 56 | targetSquare, 57 | }); 58 | } 59 | 60 | // return true so that the promotion move is not animated 61 | // the downside to this is that any other moves made first will not be animated and will reset our move to be animated again e.g. if you are premoving a promotion move and the opponent makes a move afterwards 62 | return true; 63 | } 64 | 65 | // not a promotion square, try to make the move now 66 | try { 67 | chessGame.move({ 68 | from: sourceSquare, 69 | to: targetSquare, 70 | }); 71 | 72 | // update the game state 73 | setChessPosition(chessGame.fen()); 74 | 75 | // return true if the move was successful 76 | return true; 77 | } catch { 78 | // return false if the move was not successful 79 | return false; 80 | } 81 | } 82 | 83 | // handle promotion piece select 84 | function onPromotionPieceSelect(piece: PieceSymbol) { 85 | try { 86 | chessGame.move({ 87 | from: promotionMove!.sourceSquare, 88 | to: promotionMove!.targetSquare as Square, 89 | promotion: piece, 90 | }); 91 | 92 | // update the game state 93 | setChessPosition(chessGame.fen()); 94 | } catch { 95 | // do nothing 96 | } 97 | 98 | // reset the promotion move to clear the promotion dialog 99 | setPromotionMove(null); 100 | } 101 | 102 | // calculate the left position of the promotion square 103 | const squareWidth = 104 | document 105 | .querySelector(`[data-column="a"][data-row="1"]`) 106 | ?.getBoundingClientRect()?.width ?? 0; 107 | const promotionSquareLeft = promotionMove?.targetSquare 108 | ? squareWidth * 109 | chessColumnToColumnIndex( 110 | promotionMove.targetSquare.match(/^[a-z]+/)?.[0] ?? '', 111 | 8, // number of columns 112 | 'white', // board orientation 113 | ) 114 | : 0; 115 | 116 | // set the chessboard options 117 | const chessboardOptions = { 118 | position: chessPosition, 119 | onPieceDrop, 120 | id: 'piece-promotion', 121 | }; 122 | 123 | // render the chessboard 124 | return ( 125 |
133 | 142 | 143 |
144 | {promotionMove ? ( 145 |
setPromotionMove(null)} 147 | onContextMenu={(e) => { 148 | e.preventDefault(); 149 | setPromotionMove(null); 150 | }} 151 | style={{ 152 | position: 'absolute', 153 | top: 0, 154 | left: 0, 155 | right: 0, 156 | bottom: 0, 157 | backgroundColor: 'rgba(0, 0, 0, 0.1)', 158 | zIndex: 1000, 159 | }} 160 | /> 161 | ) : null} 162 | 163 | {promotionMove ? ( 164 |
177 | {(['q', 'r', 'n', 'b'] as PieceSymbol[]).map((piece) => ( 178 | 201 | ))} 202 |
203 | ) : null} 204 | 205 | 206 |
207 | 208 |

209 | Move the white pawn to the 8th rank to trigger the promotion dialog. 210 | Click the reset button to return to the initial position. 211 |

212 |
213 | ); 214 | }, 215 | }; 216 | -------------------------------------------------------------------------------- /docs/stories/advanced-examples/FourPlayerChess.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import defaultMeta from '../basic-examples/Default.stories'; 5 | import { Chessboard, defaultDraggingPieceStyle } from '../../../src'; 6 | import { defaultPieces } from '../../../src/pieces'; 7 | 8 | const meta: Meta = { 9 | ...defaultMeta, 10 | title: 'stories/FourPlayerChess', 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const FourPlayerChess: Story = { 18 | render: () => { 19 | // use position object to set up the board, using custom piece types for each player colour 20 | const topPieces = { 21 | d14: { pieceType: 'yR' }, 22 | e14: { pieceType: 'yN' }, 23 | f14: { pieceType: 'yB' }, 24 | g14: { pieceType: 'yK' }, 25 | h14: { pieceType: 'yQ' }, 26 | i14: { pieceType: 'yB' }, 27 | j14: { pieceType: 'yN' }, 28 | k14: { pieceType: 'yR' }, 29 | d13: { pieceType: 'yP' }, 30 | e13: { pieceType: 'yP' }, 31 | f13: { pieceType: 'yP' }, 32 | g13: { pieceType: 'yP' }, 33 | h13: { pieceType: 'yP' }, 34 | i13: { pieceType: 'yP' }, 35 | j13: { pieceType: 'yP' }, 36 | k13: { pieceType: 'yP' }, 37 | }; 38 | const rightPieces = { 39 | n11: { pieceType: 'gR' }, 40 | n10: { pieceType: 'gN' }, 41 | n9: { pieceType: 'gB' }, 42 | n8: { pieceType: 'gQ' }, 43 | n7: { pieceType: 'gK' }, 44 | n6: { pieceType: 'gB' }, 45 | n5: { pieceType: 'gN' }, 46 | n4: { pieceType: 'gR' }, 47 | m11: { pieceType: 'gP' }, 48 | m10: { pieceType: 'gP' }, 49 | m9: { pieceType: 'gP' }, 50 | m8: { pieceType: 'gP' }, 51 | m7: { pieceType: 'gP' }, 52 | m6: { pieceType: 'gP' }, 53 | m5: { pieceType: 'gP' }, 54 | m4: { pieceType: 'gP' }, 55 | }; 56 | const bottomPieces = { 57 | d1: { pieceType: 'rR' }, 58 | e1: { pieceType: 'rN' }, 59 | f1: { pieceType: 'rB' }, 60 | g1: { pieceType: 'rQ' }, 61 | h1: { pieceType: 'rK' }, 62 | i1: { pieceType: 'rB' }, 63 | j1: { pieceType: 'rN' }, 64 | k1: { pieceType: 'rR' }, 65 | d2: { pieceType: 'rP' }, 66 | e2: { pieceType: 'rP' }, 67 | f2: { pieceType: 'rP' }, 68 | g2: { pieceType: 'rP' }, 69 | h2: { pieceType: 'rP' }, 70 | i2: { pieceType: 'rP' }, 71 | j2: { pieceType: 'rP' }, 72 | k2: { pieceType: 'rP' }, 73 | }; 74 | const leftPieces = { 75 | a11: { pieceType: 'bR' }, 76 | a10: { pieceType: 'bN' }, 77 | a9: { pieceType: 'bB' }, 78 | a8: { pieceType: 'bK' }, 79 | a7: { pieceType: 'bQ' }, 80 | a6: { pieceType: 'bB' }, 81 | a5: { pieceType: 'bN' }, 82 | a4: { pieceType: 'bR' }, 83 | b11: { pieceType: 'bP' }, 84 | b10: { pieceType: 'bP' }, 85 | b9: { pieceType: 'bP' }, 86 | b8: { pieceType: 'bP' }, 87 | b7: { pieceType: 'bP' }, 88 | b6: { pieceType: 'bP' }, 89 | b5: { pieceType: 'bP' }, 90 | b4: { pieceType: 'bP' }, 91 | }; 92 | 93 | // combine the pieces into a single position object 94 | const [position] = useState({ 95 | ...topPieces, 96 | ...rightPieces, 97 | ...bottomPieces, 98 | ...leftPieces, 99 | }); 100 | 101 | // track the orientation of the board in terms of degrees of rotation 102 | const [orientation, setOrientation] = useState(0); 103 | 104 | // hide 9 squares in each corner 105 | useEffect(() => { 106 | const corners = [ 107 | // Top-left 108 | { files: ['a', 'b', 'c'], ranks: ['12', '13', '14'] }, 109 | // Top-right 110 | { files: ['l', 'm', 'n'], ranks: ['12', '13', '14'] }, 111 | // Bottom-left 112 | { files: ['a', 'b', 'c'], ranks: ['1', '2', '3'] }, 113 | // Bottom-right 114 | { files: ['l', 'm', 'n'], ranks: ['1', '2', '3'] }, 115 | ]; 116 | 117 | // loop through each corner and hide the squares 118 | corners.forEach((corner) => { 119 | corner.files.forEach((file) => { 120 | corner.ranks.forEach((rank) => { 121 | const el = document.getElementById( 122 | 'four-player-chess-square-' + file + rank, 123 | ); 124 | if (el) { 125 | el.style.display = 'none'; 126 | } 127 | }); 128 | }); 129 | }); 130 | }, []); 131 | 132 | // define the styles for each player colour and the rotation of the pieces 133 | const yellowStyle = { 134 | fill: '#FFD700', 135 | svgStyle: { transform: `rotate(${-orientation}deg)` }, 136 | }; 137 | const greenStyle = { 138 | fill: '#00A86B', 139 | svgStyle: { transform: `rotate(${-orientation}deg)` }, 140 | }; 141 | const redStyle = { 142 | fill: '#D7263D', 143 | svgStyle: { transform: `rotate(${-orientation}deg)` }, 144 | }; 145 | const blueStyle = { 146 | fill: '#1E90FF', 147 | svgStyle: { transform: `rotate(${-orientation}deg)` }, 148 | }; 149 | 150 | // define the pieces for each player colour 151 | const fourPlayerPieces = { 152 | // Yellow (top) 153 | yP: () => defaultPieces.wP(yellowStyle), 154 | yR: () => defaultPieces.wR(yellowStyle), 155 | yN: () => defaultPieces.wN(yellowStyle), 156 | yB: () => defaultPieces.wB(yellowStyle), 157 | yQ: () => defaultPieces.wQ(yellowStyle), 158 | yK: () => defaultPieces.wK(yellowStyle), 159 | // Green (right) 160 | gP: () => defaultPieces.wP(greenStyle), 161 | gR: () => defaultPieces.wR(greenStyle), 162 | gN: () => defaultPieces.wN(greenStyle), 163 | gB: () => defaultPieces.wB(greenStyle), 164 | gQ: () => defaultPieces.wQ(greenStyle), 165 | gK: () => defaultPieces.wK(greenStyle), 166 | // Red (bottom) 167 | rP: () => defaultPieces.wP(redStyle), 168 | rR: () => defaultPieces.wR(redStyle), 169 | rN: () => defaultPieces.wN(redStyle), 170 | rB: () => defaultPieces.wB(redStyle), 171 | rQ: () => defaultPieces.wQ(redStyle), 172 | rK: () => defaultPieces.wK(redStyle), 173 | // Blue (left) 174 | bP: () => defaultPieces.wP(blueStyle), 175 | bR: () => defaultPieces.wR(blueStyle), 176 | bN: () => defaultPieces.wN(blueStyle), 177 | bB: () => defaultPieces.wB(blueStyle), 178 | bQ: () => defaultPieces.wQ(blueStyle), 179 | bK: () => defaultPieces.wK(blueStyle), 180 | } as const; 181 | 182 | // set the chessboard options 183 | const chessboardOptions = { 184 | chessboardRows: 14, 185 | chessboardColumns: 14, 186 | position, 187 | id: 'four-player-chess', 188 | pieces: fourPlayerPieces, 189 | showNotation: false, 190 | boardStyle: { 191 | transform: `rotate(${orientation}deg)`, 192 | }, 193 | draggingPieceStyle: { 194 | ...defaultDraggingPieceStyle, 195 | transform: `rotate(${orientation}deg)`, // rotate the dragging piece to match the orientation of the board 196 | }, 197 | }; 198 | 199 | // render the chessboard 200 | return ( 201 |
209 |
210 | 4-Player Chess (Board Only) 211 |
212 |
213 | 214 | 215 | 216 | 217 |
218 | 219 |
220 | ); 221 | }, 222 | }; 223 | --------------------------------------------------------------------------------