unsung
v1.1.1
Published
Display credits and signature in the browser console with custom styling options.
Maintainers
Readme
Unsung.ts
Unsung is a framework-agnostic way to make project attribution visible from the console. Credit data lives in a small JSON file, styling is provided in code, and the output adapts to the runtime: a full styled console.groupCollapsed in the browser, a lightweight unstyled console.group in Node.js, with zero runtime dependencies.
Built for curious developers who open DevTools on a published project and want to discover the people behind the work.
Created and maintained by Thaddeus Ex Ars.
📑 Table of contents
Features · Installation · Quick start · Data format · Browser and node · Theming · API · Contributing · License
✨ Features
- 🪧 Data driven: declare credits once in JSON, group people by role automatically
- 🎨 Themeable: text colors, header background and font sizes, via presets or raw hex
- 🌍 Runtime aware: styled output in the browser, compact in Node, off by default server side
- 🔒 Safe: prints once per runtime, never throws, fully typed
📦 Installation
pnpm add unsung⚡ Quick start
import { printCredits } from 'unsung'
// Usually imported from a generated JSON file:
// import credits from './credits.json'
const credits = {
header: {
siteName: 'Foo Studio',
typeOfProject: 'A web experience',
clientName: 'Foo'
},
sections: [
{
title: 'Creative',
entries: [
{ role: 'Creative Director', entity: 'Sam Smith', handle: '@samsmith' },
{ role: 'Designer', entity: 'Marie Chen', handle: '@mchen' },
{ role: 'UX Engineer', entity: 'Jane Doe', handle: '@janedoe' },
{ role: 'UX Engineer', entity: 'John Doe' }
]
},
{
title: 'Technology',
entries: [{ role: 'Technical Director', entity: 'Thaddeus Ex Ars', handle: '@thddexars' }]
}
],
libs: ['Vite', 'GSAP'],
footer: { year: '2026', owner: 'Thaddeus Ex Ars' }
}
printCredits({ data: credits })In the browser this opens a single collapsed group titled Credits with a styled sidebar and formatted text:
📋 Data format
The credits payload is plain data only. No styling lives here, so the same data can be themed differently per project.
type CreditsData = {
header: {
siteName: string
typeOfProject?: string // optional free string: "website", "web experience", "package"...
clientName?: string // optional
}
sections?: {
// optional
title: string
entries: {
role: string
entity: string // a person or an organization
handle?: string // optional pseudo, e.g. "@ThddExArs"
}[]
}[]
libs?: string[] // optional
footer?: {
// optional
year: string
owner: string // copyright holder shown in the footer
}
}A few rules:
- Only
header.siteNameis required. Everything else is optional: omittypeOfProject,clientName,sections,libs, orfooterto skip them without error. - No URLs. They are too noisy in a console. The only extra you can attach to a person is a
handle. - Roles are grouped within their section: entries that share the same
roleare printed together, in first seen order. - Empty values are skipped. Empty libs are dropped, and the
Built withblock is omitted when there are none. - Every string is sanitized before printing: control characters are stripped, whitespace is trimmed, length is capped, and percent signs are handled so they cannot be read as console format specifiers.
🌍 Browser and node
The runtime is detected automatically.
| Runtime | Output |
| ------- | --------------------------------------------------------------------------------------- |
| Browser | Full styled console.groupCollapsed: header, every section, libs, footer |
| Node | Light unstyled console.group: header, libs, footer only (person sections are skipped) |
| Unknown | Nothing is printed |
Node printing is off by default. This matters for server side rendering (Next, Nuxt, SvelteKit), where component code runs on the server on every request. Leaving node off keeps the credits a browser only feature and your server logs clean. Turn it on explicitly when you want it:
printCredits({ data: credits, enablePrintInNode: true })🎨 Theming
Styling is passed in code through the optional theme. It is merged over a built in default theme, so you only specify what you want to override.
printCredits({
data: credits,
theme: {
colors: {
// one value for all text, or an object targeting the three levels
text: {
level1Title: 'softWhite', // the site name, light on the dark background
level2Title: 'inkBlue', // section and "Built with" titles, dark
body: 'charcoal' // names, libs, footer, dark
},
background: 'blueNight' // applied to the site name line only
},
fontSizes: {
level1Title: 18, // px
level2Title: 13, // px
body: 12 // px
}
}
})Color values
Each color is either a preset name or a raw hex string (#RGB or #RRGGBB).
Text presets, light and dark:
| Light text | Dark text |
| --------------- | -------------- |
| softWhite | charcoal |
| iceBlue | inkBlue |
| lightMint | deepForest |
| lightLavender | darkBurgundy |
| paleYellow | espresso |
| softPink | graphite |
Background presets, dark and light:
| Dark background | Light background |
| --------------- | ---------------- |
| blueNight | paperWhite |
| deepGreen | skyMist |
| darkPlum | lightSand |
| burntAmber | blushLight |
Pair a light background with dark text, or a dark background with light text, for readable contrast.
// a single value applies to all three text levels
theme: { colors: { text: 'charcoal' } }
// raw hex works anywhere a preset name does
theme: { colors: { text: { body: '#DDD6FE' }, background: '#0F172A' } }The maps and the default theme are exported if you want to read or extend them:
import { DEFAULT_THEME, textColorMap, backgroundColorMap } from 'unsung'⚙️ API
printCredits(options)
Prints the credits to the console. Returns nothing and never throws.
| Option | Type | Default | Description |
| ------------------- | -------------- | --------------- | ----------------------------------------------------------------------------- |
| data | CreditsData | required | The credits payload |
| theme | ConsoleTheme | DEFAULT_THEME | Styling overrides, merged over the default theme |
| groupTitle | string | 'Credits' | Label of the console group wrapping the output |
| enablePrintInNode | boolean | false | Enable printing in node environments. Off by default to avoid SSR log spam |
| logErrors | boolean | false | Surface internal failures as a console.warn instead of staying silent |
| collapsed | boolean | true | In browser, use groupCollapsed (default) or group (open). Node unaffected |
Group title
Use groupTitle to replace the default Credits label with something more specific to your project:
printCredits({ data: credits, groupTitle: 'acme-ui credits' })Failure behavior
The library is built so it can never break the host app. Every internal error (a malformed hex color, a missing field, bad data) is caught at the single public boundary and swallowed by default, the credits are simply skipped.
Set logErrors: true to surface a console.warn explaining what was skipped, so you can fix your config while debugging. You decide when to enable it, there is no NODE_ENV guessing. The library never throws either way.
Print once
The credits print at most once per runtime. A module re-import, a hot reload, or a component remount will not duplicate them.
👥 Contributing
Please read CONTRIBUTING.md.
📜 License
Apache-2.0 © Ludovic Bouvinet (Thaddeus Ex Ars)
