| Directory | Description |
|---|---|
| src | Theme generation framework and utilities |
| src/core | Theme generation framework (CSS vars, syntax highlighting styles) |
| src/functions | Theme utilities (color calculations, etc.) |
| src/palette | Theme palettes (GitHub / Gitea / Catppuccin) |
| src/types | Theme color variable type definitions |
| src/vite-plugin | Theme Vite build plugin |
| styles | Theme-wide overrides in TypeScript, built into every theme stylesheet |
| styles/common | Reusable CSS style fragments |
| styles/public | Base element or cross-page element styles |
| styles/components | Overrides of Forgejo pages by Forgejo's own markup |
| styles/templates | Styles for markup only the theme's older repository templates render |
| styles/pages | Page-only CSS for the theme's templates |
| components | Reusable UI: template partials, stylesheet, browser script and tests |
| templates | Forgejo template overrides (optional) |
| public | Stylesheets and scripts that one page loads from its template |
| themes | Color theme definitions |
| primer | Auto-generated Primer design tokens |
| scripts | Build and utility scripts, including the template pack assembly |
| dev | Local Forgejo preview and its fixtures |
| .github/assets/screenshots | Theme screenshots |
ARCHITECTURE.md explains how these parts are built into the theme stylesheets and the template pack.
Style contributions are not recommended, as the theme-wide overrides are built into every theme stylesheet and the shared components load on every page, which introduces issues like ordering/override conflicts and broad-reaching style impacts.
Each developer has their own approach, making it difficult for review to catch these issues, greatly increasing maintenance difficulty.
If you have confirmed understanding of Forgejo's markup and the project's approach, please first file an Issue to confirm scope and expected results, then develop and submit a PR.
If you believe you have a better approach, feel free to submit an Issue.
Important
When submitting a PR, please run bun commit
Submit changes through a pull request targeting dev, or stacked on another pull request; checks run for both. A merged
pull request lands in the release draft as * <title> ([#<number>](<url>)), under the category of its label. The draft
is written for the people who install the theme, so the title and the label decide what they read.
- Use the strict format
Area: Description, with a past-tense description of what changed for the reader:Introduced,Changed,Fixed,Removed. - Describe the effect, not the implementation:
Branches: Fixed the pull request column on phones, notBranches: Changed branch.css. - The draft sorts by the exact area, so spell an area exactly as listed below.
Areas: Actions, Auth, Branches, Code, Commits, Dashboard, Editor, Explore, Forks, Forms, Insights,
Issues, Labels, Milestones, Navbar, Notifications, Organizations, Packages, Profile, Projects,
Pull requests, Releases, Repository, Settings, Teams, Templates, Themes, Wiki.
Reuse an existing area whenever one fits; add a new area only for a page people recognise by that name, and add it to this list in the same pull request. Never use file, component or style names as an area.
| Label | Use for | Draft section |
|---|---|---|
feature |
a page, control or theme that is new to users | What’s new |
change |
a changed look, template or compatibility | Enhancements |
bug |
a fix for a bug that shipped in a release | Fixes |
dependencies |
a dependency or build-tool update | Dependencies |
skip-changelog |
anything a reader of the release would not notice | not listed |
Apply exactly one of them; the pull request metadata check enforces it with the title format. A fix for a bug that only
existed on dev is skip-changelog. Add major when admins must act on upgrade, for example a new minimum Forgejo
version; it raises the major version and does not add a section. suggestion marks an idea that has not been decided on
yet.
The summary and the compatibility section at the top of the draft come from .github/release-notes.md when its front
matter names the version the draft resolves to. Update it in the pull request that needs upgrade steps or warnings,
instead of editing the draft.
A pull request that changes how a page looks shows it in a Before and after table in its description: the same page, viewport and state, captured in the dev preview.
The theme depends on the Bun runtime. Please ensure Bun is installed in your environment. Bun 1.3.14 or later is recommended.
Please use VSCode for development and install the recommended extensions in the repository.
If you prefer fewer extensions, you must install the vscode-styled-components extension, which is used for rendering
and checking CSS template strings in TypeScript.
VSCode 1.102.0 or later is recommended for development, as this version and above support hex color rendering in TypeScript code.
bun installCreate a .env file in the project root. Refer to .env.example for available variables.
Environment variables are used to send compiled themes to a server for quick preview.
To check changes in a running Forgejo, use the local preview described in README.md. To compile and sync to a remote server instead:
bun devCompile all themes:
bun bundleFormat project code:
bun fmtFor PR submission — check and compile all project code with formatting:
bun commitThe files in templates are Forgejo's templates with the theme's changes. The forgejo field of package.json pins
the Forgejo release they were copied from; the dev preview runs the same release, and a test checks that
dev/docker-compose.yml names its image.
bun drift checks out the templates of the pinned release into node_modules/.cache/forgejo (git and network on the
first run only) and lists each override by the lines the theme added and removed, the largest first, then the templates
Forgejo does not have. bun drift 17.1.0 does the same against another release.
To move to a new release:
- Run
bun drift <pinned> <new>, for examplebun drift 17.0.0 17.1.0. It lists the overridden templates Forgejo changed between the two releases, with Forgejo's diff of each, and the ones it added or removed. - Port each change into the override, and rename or remove an override whose file Forgejo moved or removed.
- Set
forgejoinpackage.jsonand the image indev/docker-compose.ymlto the new release, then check the changed pages in the dev preview.
src, styles, and themes are the project's main directories.
When referencing across modules, use package exports paths. For example:
import { defineTheme } from "@lutinglt/gitea-github-theme/core"
When referencing files within the same module, use relative paths. For example: import { defineTheme } from "./theme"
Theme file naming format: themeName.ts, used together with theme.config.ts registration.
If a theme has both dark and light variants, the framework automatically generates an auto color theme.
The project accepts custom themes and will include them in published releases, but the project owner does not participate in their maintenance or review.
Please add your author info at the top of the color theme file so Issue reporters can @ mention you.
It is recommended to use defineTheme with the ThemeColor type to define theme colors. The framework will
automatically compute and generate all Gitea CSS variables.
Color calculation functions can be imported from @lutinglt/gitea-github-theme/core (scaleColorLight, rgba), or use
the color2k library for additional color manipulation.
Example: themes/my-theme.ts
/**
* @author your-name
* @description theme description
*/
import { defineTheme, scaleColorLight } from "@lutinglt/gitea-github-theme/core";
import type { ThemeColor, Syntax } from "@lutinglt/gitea-github-theme/core";
const themeColor: ThemeColor = {
isDarkTheme: true,
primary: "#0969da",
primaryContrast: "#f0f6fc",
secondary: "#30363d",
base: { red: "#f85149", orange: "#d29922" /* ... */ },
console: {
/* ... */
},
diff: {
/* ... */
},
other: {
/* ... */
},
github: {
/* ... */
},
};
const syntaxColor: Syntax = {
/* Custom code highlight colors, or import from another theme */
};
export default defineTheme({ colorType: "theme", themeColor, syntaxColor });To use Gitea's native color format, use colorType: "gitea" with the GiteaColor type:
import type { GiteaColor, Syntax } from "@lutinglt/gitea-github-theme/core";
import { defineTheme } from "@lutinglt/gitea-github-theme/core";
const giteaColor: GiteaColor = {
/* ... */
};
const syntaxColor: Syntax = {
/* ... */
};
export default defineTheme({ colorType: "gitea", themeColor: giteaColor, syntaxColor });After completing theme color development, register the theme series in theme.config.ts, compile it, and take
screenshots in a Forgejo instance. Place them in the .github/assets/screenshots directory with filenames matching the
theme names. (Cloning the repository is recommended to avoid leaking personal information.)
Then add screenshot information to README.md.
For color reuse patterns, see the colorblind theme implementation in themes/github.ts.
The TypeScript styles in styles/ use the css() template string for development. Strings are processed by
LightningCSS and support CSS nesting syntax. Do not use SCSS functions; for complex processing, use TypeScript libraries
such as the bundled color2k library.
For complex logic, it is recommended to extract it into functions under the src/functions directory.
Choose the place by the markup a rule styles and the pages that need it:
- Forgejo's own markup, on any page: a
cssfragment instyles/, understyles/publicfor base elements orstyles/componentsfor one area of Forgejo. It is built into every theme stylesheet, so installs without the templates get it too. - Reusable UI: a directory under
components/<group>/<name>/with its template partials, stylesheet, script and tests. Link its stylesheet and script intemplates/base/component_assets.tmpl;bun bundlecopies the files. - Layout of one page or a family of pages: plain CSS in
styles/pages/<page>.css. Add the file to the list inscripts/assembleForgejo.tsand link it intemplates/base/component_assets.tmpl. - An asset that one page template loads:
public/<page>.cssorpublic/assets/js/<page>.js, linked from that template. Styles that several pages share belong in a component.
Link assets as {{AssetUrlPrefix}}/css/... or {{AssetUrlPrefix}}/js/... without a version; the build adds the content
hash.
Name the classes the theme adds by their scope:
theme-*for shared components, such astheme-search-fieldandtheme-counter, withdata-theme-*attributes as script hooks.github-*for layout and state hooks on overridden templates, such asgithub-issuesorgithub-embedded.- A page prefix such as
profile-,org-oractions-for page-only CSS, such asprofile-avatar-card.
Keep stacked layers within the scale in components/common/motion/motion.css:
| Layer | z-index |
|---|---|
| Popovers and positioned page content | up to 100 |
Repository tabs (.repository-tab-nav) |
199 |
Navbar (#navbar) |
200 |
| Toasts | 500 |
| Dimmers and modals | 1000 and up |
Use components/common/*/*.css for sidebars, search fields, selects, counters, switches, empty states, and compact
buttons, and components/common/dropdown/* for dropdown variants. templates/base/component_assets.tmpl, included from
templates/base/head_navbar.tmpl, loads the shared styles and controllers for all pages. Keep page layout rules in the
page stylesheet and pass the page's existing routes, permissions, and form values into the shared controls.
- Sidebar: add
theme-sidebar-layoutand a uniquedata-theme-sidebarstorage key to the page, then usetheme-sidebar,theme-sidebar-content, andtheme-sidebar-mainfor its children. Includecomponents/common/sidebar/toggleinside the sidebar and a button withdata-theme-sidebar-toggleanddata-theme-sidebar-expandbeside the page title. The controller preserves desktop collapse preferences and starts collapsed on mobile. Seetemplates/repo/issue/sidebar.tmplandtemplates/org/repository_nav.tmpl. For links to sections of a long page, like GitHub's "On this page", add anavwithdata-theme-sidebar-outlineholding atheme-sidebar-labelheading and#idlinks; the controller marks the section in view and, on a phone, closes the sidebar when an entry is followed. Seetemplates/repo/release/sidebar.tmpl. - Search: call
components/common/search/searchwithValueandPlaceholderinside the existing form; do not write a search input in a page template.Kindselects the variant:submit(default, trailing search button),icon(leading search icon, as on releases and Actions), orplain.Namedefaults toq,Labeloverrides the accessible name, andClassadds a page layout hook to the field. For a client-side filter that is never submitted, passLocalwith anIDfor its controller. For results that htmx loads while typing, passLive(the URL) andLiveTarget(the results' selector), as the wiki search does. Keep hidden filter inputs in the form and size the field from the page stylesheet through itsClassor parent; an unconstrained wrapper can shrink it. A control built by a script uses thetheme-search-fieldclass directly. Forgejo's native query/mode/submit group (shared/search/combo_multi) opts in withtheme-search-controlon its form. - Dropdowns: call
components/common/dropdown/dropdownwithKind(single,multiple, oractions),Label,Items(the content template name), andData(the page context).Title,Icon,IconOnly,UpdateLabel,Quiet, andPrimary(a green trigger, as on GitHub's Code menu) customize the header and trigger;Classadds a page layout hook to the menu. Usecomponents/common/dropdown/optionfor radio or checkbox rows; action rows are links or buttons withtheme-dropdown-action. Single-choice menus use checkmarks, multiple-choice menus use native checkboxes, and action menus omit selection indicators. The controller handles Escape, outside clicks, and opening one menu at a time. Seecomponents/repository/filter/filter,components/repository/sort/sort, andorg/action_menu_items. - Buttons: keep Forgejo's native
ui buttonclasses on buttons Forgejo already renders; the theme styles those globally. For a button the theme adds, usebutton theme-control(32px, 12px inline padding) instead of sizing it in a page stylesheet, withtheme-control-widefor the 16px padding GitHub uses on profile pages ortheme-control-smallfor 28px controls. Usetheme-icon-buttononly for icon-only controls. Keep nativeprimary,secondary, and permission checks on actions, and leave page stylesheets to layout such asflexandorder. - Counter: add
theme-counterto a count pill next to a tab, heading, or filter, including ones a script creates. Addtheme-counter-tabinside a tab, where GitHub's counter is 18px tall and semibold. Keep only layout (margin,flex) under the page's own class. - Empty state: use
theme-emptyfor a bordered "nothing here" box holding an icon, anh2orh3, and an optional paragraph.templates/user/profile_empty.tmplrenders one fromIcon,Title, andDescription. Addtheme-empty-compactfor the smaller, borderless block GitHub shows inside a list. Short inline messages inside a list or menu stay plain text. - Label: use Forgejo's native
ui basic labelfor outline labels such as visibility or package type; do not build a page-specific pill. - Select: wrap a native
<select class="theme-select">in<span class="theme-select-wrap">. The options stay in the page template. Seetemplates/user/profile_package_versions.tmpl.
For repository filtering, add data-theme-repository-search to the form and render
components/repository/results/results after it. Its results.js updates that result fragment through the same
server-rendered GET route, preserves search and sort values, and cancels obsolete requests. Checkbox criteria combine,
while opposing values of one criterion are exclusive. Clear filters is an action, not a selectable option. Keep
tab=overview or tab=repositories in the organization form; query parameters alone do not determine the page layout.
Use org/repository_pagination so pagination retains the same view even without the enhancement script. Errors leave
the previous results visible and display a retry message; browser history restores the server-rendered controls and
results.
After changing a component, verify each consuming page at desktop and mobile widths. Check search submission, retained filters, sidebar focus and collapse behavior, and Escape/outside-click dismissal for select menus.
All colors used in theme styles must use color variables. Import them via
import { themeVars } from "@lutinglt/gitea-github-theme/core".
When using theme-specific color variables like ${themeVars.github.xxx}, please add the file and variable to the
corresponding variable's comment in src/types/color/github.
For small border-radius values (6px), use the global border-radius variable. Import via
import { otherThemeVars } from "@lutinglt/gitea-github-theme/core" and use ${otherThemeVars.border.radius}.