{"id":41641267,"url":"https://github.com/zetter/react-crossword","last_synced_at":"2026-01-24T15:10:39.969Z","repository":{"id":33619130,"uuid":"158267902","full_name":"zetter/react-crossword","owner":"zetter","description":"React Crossword Component","archived":false,"fork":false,"pushed_at":"2023-03-06T15:02:58.000Z","size":1237,"stargazers_count":30,"open_issues_count":23,"forks_count":16,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-07-07T09:09:40.229Z","etag":null,"topics":["crosswords","puzzles","react-comments"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zetter.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-11-19T17:52:47.000Z","updated_at":"2024-10-12T18:43:23.000Z","dependencies_parsed_at":"2024-06-21T17:54:12.234Z","dependency_job_id":"4ba92f4f-01b9-474f-87b4-6883c06437ec","html_url":"https://github.com/zetter/react-crossword","commit_stats":{"total_commits":87,"total_committers":3,"mean_commits":29.0,"dds":"0.11494252873563215","last_synced_commit":"616e4530a09fcf786d88342ba6be44d3731fca3c"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zetter/react-crossword","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zetter%2Freact-crossword","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zetter%2Freact-crossword/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zetter%2Freact-crossword/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zetter%2Freact-crossword/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zetter","download_url":"https://codeload.github.com/zetter/react-crossword/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zetter%2Freact-crossword/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28730315,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-24T10:24:43.181Z","status":"ssl_error","status_checked_at":"2026-01-24T10:24:36.112Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crosswords","puzzles","react-comments"],"created_at":"2026-01-24T15:10:39.398Z","updated_at":"2026-01-24T15:10:39.956Z","avatar_url":"https://github.com/zetter.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# React Crossword\n\n----\n\n⚠️ This project is no longer maintained. Some alternatives:\n+ [MyCrossword](https://www.npmjs.com/package/mycrossword) by Tom Blackwell\n+ [React Crossword](https://www.npmjs.com/package/@guardian/react-crossword) by The Guardian\n+ [React Crossword](https://www.npmjs.com/package/@jaredreisinger/react-crossword) by Jared Reisinger\n\n----\n\nThis is a React crossword component extracted from the [Guardian Frontend application](https://github.com/guardian/frontend).\n\n[Package on NPM](https://www.npmjs.com/package/react-crossword).\n\n![Example of React Crossword](https://chriszetter.com/assets/react-crossword/react-crossword-example.png)\n\n## Features\n\n+ Displaying a grid and clues\n+ Displaying separators in the gird for hyphenated and multiple-word answers\n+ Displaying attempted clues as greyed out\n+ Responsive to different screen sizes\n+ Clicking on clue highlights row or column\n+ Grouping clues together for clues that span multiple columns or rows\n+ Tabbing highlights the previous or next clue\n+ Deep-linking to individual clues with URL fragments\n+ Arrow keys can be used to navigate between cells\n+ Saving progress to local storage\n+ Smart clearing that only clears cells not part of other completed answers\n+ Checking and revealing answers (if provided)\n+ Anagram helper\n\n## API\n\n### Props\n\n| name  | information |\n|---|---|\n| `data` | Required. This contains crossword clues, answers and other information needed to draw the grid. See **Crossword Data** below for more. |\n| `loadGrid` | Optional function to override storage mechanism. Called when the component is initialized with the ID of the crossword. Should return an array-based representation of the crossword grid. See **The Grid** below for more.  |\n| `saveGrid` | Optional function to override storage mechanism. Called after the grid has changed with the ID of the crossword and array-based representation of the grid. See **The Grid** below for more. |\n| `onMove` | Optional function. Called after a grid cell has changed with an object representing the move. The object contains the properties `x`, `y`, `value` and `previousValue`. |\n| `onFocusClue` | Optional function. Called after a clue receives focus via a cell change. The object returned contains the properties `x`, `y` and `clueId`. |\n\n### Functions\n\nThere are publicly accessible functions in the `Crossword` component that help manage the state of the crossword. They can be accessed from a [React `Ref`](https://reactjs.org/docs/refs-and-the-dom.html). In particular, two are useful if loading grid data from other sources:\n\n#### setCellValue\n```js\nsetCellValue(x, y, value, triggerOnMoveCallback = true)\n```\n\nCall to fill in an individual cell. This will trigger the `onMove` callback unless the `triggerOnMoveCallback` parameter is set to false.\n\n\n#### updateGrid\n```js\nupdateGrid(gridState)\n```\n\nThis can be used to update the state of the whole grid. The `onMove` callback will not be called. See **The Grid** below for more.\n\n### Crossword Data\n\nHere's an example set of data to create the crossword pictured above and in the included example:\n\n```js\n{\n  id: 'simple/1',\n  number: 1,\n  name: 'Simple Crossword 1',\n  date: 1542326400000,\n  entries: [\n    {\n      id: '1-across',\n      number: 1,\n      humanNumber: '1',\n      clue: 'Toy on a string (2-2)',\n      direction: 'across',\n      length: 4,\n      group: ['1-across'],\n      position: { x: 0, y: 0 },\n      separatorLocations: {\n        '-': [2],\n      },\n      solution: 'YOYO',\n    },\n    {\n      id: '2-across',\n      number: 2,\n      humanNumber: '2',\n      clue: 'Have a rest (3,4)',\n      direction: 'across',\n      length: 7,\n      group: ['2-across'],\n      position: { x: 0, y: 2 },\n      separatorLocations: {\n        ',': [3],\n      },\n      solution: 'LIEDOWN',\n    },\n    {\n      id: '1-down',\n      number: 1,\n      humanNumber: '1',\n      clue: 'Colour (6)',\n      direction: 'down',\n      length: 6,\n      group: ['1-down'],\n      position: { x: 0, y: 0 },\n      separatorLocations: {},\n      solution: 'YELLOW',\n    },\n    {\n      id: '3-down',\n      number: 3,\n      humanNumber: '3',\n      clue: 'Bits and bobs (4,3,4)',\n      direction: 'down',\n      length: 7,\n      group: ['3-down', '4-down'],\n      position: { x: 3, y: 0 },\n      separatorLocations: {\n        ',': [4],\n      },\n      solution: 'ODDSAND',\n    },\n    {\n      id: '4-down',\n      number: 4,\n      humanNumber: '4',\n      clue: 'See 3 down',\n      direction: 'down',\n      length: 4,\n      group: ['3-down', '4-down'],\n      position: {\n        x: 6,\n        y: 1,\n      },\n      separatorLocations: {},\n      solution: 'ENDS',\n    },\n  ],\n  solutionAvailable: true,\n  dateSolutionAvailable: 1542326400000,\n  dimensions: {\n    cols: 13,\n    rows: 13,\n  },\n  crosswordType: 'quick',\n}\n```\n\n### The Grid\n\nSome functions require or return the state of the crossword grid. This is a 2-dimensional array holding the values in each cell. Incomplete cells or cells that are not part of any answer are represented as the empty string (`\"\"`). Note that it follows the convention of indexing by column first (x) and then row (y) so the printed array is transposed compared to how the crossword grid appears. For example, a crossword of:\n\n\u003ctable\u003e\n\u003ctr\u003e\n    \u003ctd\u003e■\u003c/td\u003e\n    \u003ctd\u003eD\u003c/td\u003e\n    \u003ctd\u003e■\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e■\u003c/td\u003e\n    \u003ctd\u003eO\u003c/td\u003e\n    \u003ctd\u003e■\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003eA\u003c/td\u003e\n    \u003ctd\u003eG\u003c/td\u003e\n    \u003ctd\u003eE\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\nwould be represented as:\n\n```JSON\n[\n [\"\",  \"\",  \"A\"],\n [\"D\", \"O\", \"G\"],\n [\"\",  \"\",  \"E\"],\n]\n```\n\n## Architecture\n\nThis component builds an SVG crossword grid and displays clues next to it.\nAn input element captures typing and is moved around the grid as different clues\nor cells are selected. There are hidden input elements before and after this to\ncapture tab key presses and trigger navigating to the next clue.\n\nOutline of the structure of rendered components:\n```html\n\u003cCrossword\u003e\n  \u003cGrid\u003e (stateless component to render SVG element)\n    \u003cCell/\u003e\n    ...\n  \u003c/Grid\u003e\n\n  \u003cHiddenInput/\u003e\n\n  \u003cControls\u003e\n    \u003cConfirmButton/\u003e\n    ...\n  \u003c/Controls\u003e\n\n  \u003cAnagramHelper\u003e (sometimes displayed)\n    \u003cClueInput/\u003e or \u003cRing/\u003e\n    \u003cCluePreview/\u003e\n  \u003c/AnagramHelper\u003e\n\n  \u003cClues\u003e\n    \u003cClue/\u003e\n    ...\n  \u003c/Clues\u003e\n\u003c/Crossword\u003e\n```\n\n## Working with this Project\n\n`yarn install` to install dependencies\n\n`yarn test` to run lint javascript \u0026 sass and run tests\n\n`yarn examples` to build and serve an example crossword\n\n`yarn build` to package for publishing\n\n## Project History\n\nThis component was originally extracted from the [Guardian Frontend application](https://github.com/guardian/frontend) that serves all the pages on [guardian.com](guardian.com).\n\nYou can [see the work to extract the component](https://github.com/guardian/frontend/compare/master...zetter:crossword). The code was then imported to the new `react-crossword` project to make a break from the larger Frontend repository.\n\n## Known Issues\n\n+ Resizing window after loading breaks the position of input element and display of crossword.\n+ CSS is not scoped to crossword component so may interfere with other elements on the page.\n+ Grid sizes are based on predefined crossword types specified with the `$xword-grid-sizes` variable in `_vars.scss` rather than the gird sized passed in with the crossword data.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzetter%2Freact-crossword","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzetter%2Freact-crossword","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzetter%2Freact-crossword/lists"}