Configuration — ns-loading
config.js
Everything lives in html/config.js. You never need to touch HTML, CSS or JS.
Text values can be a plain string or one string per language:
tagline: 'Your story starts here'
tagline: { en: 'Your story starts here', tr: 'Hikâyen burada başlıyor' }
File paths are relative to the html/ folder, e.g. 'assets/images/logo.png'.
Mistakes are safe. A wrong type, an invalid colour or an unsafe URL is replaced by the default and reported once in the console. Open the browser preview to see a count of config warnings.
Branding & logo
brand: {
serverName: 'YOUR CITY',
tagline: { en: 'Your story starts here', tr: 'Hikâyen burada başlıyor' },
logo: '', // 'assets/images/logo.png'
logoHeight: 44, // px at 1080p, scales with resolution
monogram: '', // e.g. 'YC'; empty = initials of serverName
showServerName: true, // set false if your logo already contains the name
showTagline: true
}
To change the logo: put your file (PNG with transparency, SVG or WebP; around 2× the displayed height looks sharp) in html/assets/images/, then set logo: 'assets/images/your-logo.png'. If the logo is empty or fails to load, a typographic monogram built from your server name is shown instead.
Hero text
hero: {
eyebrow: { en: 'Welcome to Los Santos', tr: "Los Santos'a hoş geldin" },
title: { en: 'Your next\n*chapter* begins', tr: 'Yeni *bölümün*\nbaşlıyor' },
subtitle: { en: 'Los Santos awaits. Make your story count.', tr: '…' },
stats: [
{ live: 'players', label: { en: 'In the city', tr: 'Şehirde' } },
{ live: 'discordOnline', label: { en: 'Online on Discord', tr: "Discord'da aktif" } },
{ value: '18+', label: { en: 'Age requirement', tr: 'Yaş sınırı' } }
]
},
live: {
discordInvite: '' // empty = the discord link from `links`
}
- In
title,\nstarts a new line and*word*gets the brushed-chrome style. - Very long titles shrink automatically to fit (up to 3 lines).
statsholds up to 4 short facts about your server. Use[]to hide the row.- Live stats show a pulsing dot next to the label:
live: 'players': players on the server, e.g.87/128. Needsserver/handover.lua(see handover). The number is taken when the player starts connecting.live: 'discordOnline'/live: 'discordMembers': online / total members from Discord's public invite API. No bot or token needed, but the invite must be permanent. Setlive.discordInvite(a code likechromeheartor a full link), or leave it empty to use the discord link fromlinks.- An optional
valueis shown while the live number is unavailable. Without one, the stat stays hidden until the number arrives, so the screen never shows a made-up count. - Discord counts are cached for 24 hours, so the next connection shows them instantly. In the browser preview the player count is simulated (
64/128).
Background
appearance: {
background: {
type: 'scene', // 'scene' | 'image' | 'slideshow' | 'video'
image: '', // 'assets/images/background.jpg'
slides: [], // ['assets/images/bg-1.jpg', 'assets/images/bg-2.jpg']
slideInterval: 9000,
video: '', // 'assets/videos/background.webm'
dim: 0.35, // 0–1 extra darkening for readability
kenBurns: true // slow zoom on images
},
scene: { stars: true, searchlights: true, fog: true, aircraft: true, palms: true },
grain: true
}
| Type | What to do |
|---|---|
scene | Nothing. This is the built-in skyline. |
image | Put a JPG/WebP (1920×1080 or larger, ideally under 1 MB) in html/assets/images/ and set image. |
slideshow | Put several images in html/assets/images/ and list them in slides. Broken slides are skipped. |
video | Put a WebM (VP9) file in html/assets/videos/ and set video. It plays muted and looped. |
Video notes: FiveM's browser (CEF) does not reliably play H.264 MP4. Use WebM/VP9. Keep it short (10–30 s loop), 1080p and ideally under 15 MB, because every player downloads it while connecting. A conversion example with ffmpeg:
ffmpeg -i input.mp4 -an -c:v libvpx-vp9 -b:v 2M -vf scale=1920:-2 -deadline good background.webm
If an image or video cannot be loaded, the built-in scene appears automatically. You will never get a white or empty screen.
Colours & fonts
appearance: {
accentColor: '#B8F36B', // progress bar, active tab, small markers
colors: { background: '', surface: '', text: '', textMuted: '', chrome: '' },
fonts: {
display: { family: '', src: '' }, // headings and numbers
body: { family: '', src: '' } // paragraphs and UI
}
}
- Colours must be hex (
#RRGGBBor#RGB). Empty values keep the NullSect palette. - The accent is used sparingly on purpose. A single strong colour reads as premium.
- Default fonts: Bahnschrift (headings) and Segoe UI (text). Both ship with Windows 10/11, so every FiveM player has them and nothing is downloaded.
- Custom font: put a
.woff2file inhtml/assets/fonts/and set e.g.display: { family: 'Oswald', src: 'assets/fonts/Oswald.woff2' }. Make sure the font's licence allows embedding. If it fails to load, the default font stays.
Music
audio: {
enabled: true,
tracks: [
{ src: 'assets/audio/theme.ogg', title: 'Night Drive', artist: 'Your Artist', cover: 'assets/images/cover.jpg' }
],
autoplay: true,
volume: 0.25,
shuffle: false,
rememberVolume: true
}
- Put OGG or MP3 files in
html/assets/audio/. - List them in
tracksand setenabled: true.
- Only use music you own or are licensed to use. Replace the example track before going live.
- The player is a card above the info panel: cover, title, artist, a seekable progress bar with times, and previous / play-pause / next buttons.
coveris optional: a square image (JPG/PNG/WebP) inhtml/assets/images/. Without it, or if it fails to load, a themed placeholder is shown.- With several tracks, "next" is enabled and tracks advance automatically. "Previous" restarts the track, and a second press goes back one track.
- Autoplay: the screen tries to start the music automatically. FiveM normally allows this. If autoplay is blocked, the player shows "Press play for music" with a gentle highlight and never shows a fake "playing" state.
- Missing or broken files are skipped. If none can play, the player shows "Music unavailable" with disabled controls, and the rest of the screen keeps working.
- Players can press M to mute or unmute. Their volume and mute choice is remembered (
rememberVolume). - With
enabled: false, or with no tracks, the player is hidden completely.
Tips
tips: {
enabled: true,
interval: 7000, // ms per tip (min 2500)
shuffle: false,
controls: true, // ‹ › buttons
items: [
{ en: 'Your choices shape your story.', tr: 'Seçimlerin hikâyeni şekillendirir.' },
'A plain-text tip works too.'
]
}
- Add as many tips as you like. Long tips switch to a smaller size and are clamped neatly.
- Hovering a tip pauses its countdown. Players can also use ← →.
- With an empty list or
enabled: false, the module disappears and the layout closes the gap.
Info panel
A tabbed panel on the right. Sections appear in the order you list them. Remove one or set enabled: false to hide it. title is optional, and a translated default is used.
panel: {
enabled: true,
autoRotate: 0, // ms between automatic tab changes (0 = off, min 3000)
sections: [
{ type: 'about', text: { en: '…', tr: '…' },
facts: [ { label: { en: 'Economy', tr: 'Ekonomi' }, value: { en: 'Player-driven', tr: 'Oyuncu odaklı' } } ] },
{ type: 'rules', items: [
{ title: { en: 'Respect everyone', tr: 'Herkese saygı' }, text: { en: '…', tr: '…' } },
'A rule can also be a single line'
] },
{ type: 'news', items: [
{ tag: 'Update', date: '2026-10-01', title: 'Season one is live', text: '…' }
] },
{ type: 'links', title: { en: 'Community', tr: 'Topluluk' } }, // shows the `links` block
{ type: 'team', members: [
{ name: 'Alex', role: { en: 'Founder', tr: 'Kurucu' }, avatar: 'assets/images/team/alex.png' },
{ name: 'Sam', role: { en: 'Developer', tr: 'Geliştirici' } }
] }
]
}
dateinYYYY-MM-DDformat is shown in the player's language ("1 Oct 2026" / "1 Eki 2026"). Any other text is shown as written.- Empty sections are hidden automatically. If no section has content, the panel disappears and the hero uses the full width.
- Long content scrolls inside the panel and never pushes the page.
team:avataris optional, either a file inhtml/assets/images/or anhttps://image such as a Discord avatar link. Without one, or if it fails to load, the initials are shown.- When there are more tabs than fit, the tab row scrolls (mouse wheel works too), the active tab is kept in view, and the hidden edge fades out.
Remote content
Update the news and the team from your own website, with no resource restart and no new download for players.
remote: {
url: 'https://your-city.com/loading.json',
timeout: 5000 // ms to wait before falling back
}
-
Copy
remote-content.example.json(project root) to your site, edit it and pointremote.urlat it. Both keys are optional:{ "news": [ { "tag": { "en": "Update", "tr": "Güncelleme" }, "date": "2026-10-10", "title": "…", "text": "…" } ], "team": [ { "name": "Alex", "role": { "en": "Founder", "tr": "Kurucu" }, "avatar": "https://…" } ] } -
The file must be served over https with the header
Access-Control-Allow-Origin: *, otherwise the loading screen is not allowed to read it:- nginx:
location = /loading.json { add_header Access-Control-Allow-Origin "*"; } - Apache (.htaccess):
<Files "loading.json">Header set Access-Control-Allow-Origin "*"</Files> - PHP:
header('Access-Control-Allow-Origin: *'); header('Content-Type: application/json'); - Cloudflare Pages / Netlify: a
_headersfile with/loading.jsonfollowed byAccess-Control-Allow-Origin: * - Vercel: a
headersentry for/loading.jsoninvercel.json
- nginx:
How it behaves:
- What the file sends replaces the items of the matching panel section (
news/team). If config.js has no such section, one is added (unless you set it toenabled: false). - Every value is validated like config.js and always shown as plain text, never HTML.
- If your site is slow or down, the screen shows the last downloaded copy (kept for 24 hours), or else the content from config.js. Nothing blocks or breaks.
- In the browser preview (
?preview=1), problems with the file appear as warnings in the console.
Links
links: {
showInHeader: true,
items: [
{ type: 'discord', url: 'https://discord.gg/your-invite' },
{ type: 'website', url: 'https://your-city.com' },
{ type: 'store', url: 'https://store.your-city.com', label: { en: 'Store', tr: 'Mağaza' } }
]
}
- Types:
discord,website,store,instagram,youtube,tiktok,x,link. - Only
http://andhttps://addresses are accepted. A bare domain such asdiscord.gg/abcis upgraded to https. Anything else is rejected and logged as a config warning. - Links appear as icon buttons in the header (with a tooltip showing the domain) and in the
linkspanel section (showing the full domain, so players can also type it). - In FiveM, links open in the player's own browser. The loading screen itself never navigates away.
Language
locale: 'en' // 'en' or 'tr'
- All interface strings live in
html/locales/en.jsandhtml/locales/tr.js. Your own content (titles, tips, rules…) uses{ en: '…', tr: '…' }objects inconfig.js. - If a translation is missing, English is used.
- Adding a language: copy
locales/en.jsto e.g.locales/de.js, change.en =to.de =and translate it. Then add<script src="locales/de.js"></script>toindex.htmlnext to the other locale scripts, and setlocale: 'de'. The manifest already includeshtml/locales/*.js.
Loading progress
progress: {
showBar: true,
showPercentage: true,
showStatus: true, // "Loading game data", "Joining the session"…
showSteps: true, // real FiveM step counter, e.g. "Step 12 / 48"
showElapsed: true, // time on the loading screen
monotonic: true, // never move the bar backwards
showLogLines: false // FiveM's raw log lines (technical)
}
monotonic: true keeps the highest value FiveM has reported. FiveM's loadFraction can occasionally dip between phases, and a bar that jumps backwards looks broken. Set it to false to display the raw value.
Motion & performance
motion: {
intro: true, // opening animation (~1.4 s)
reducedMotion: 'auto', // 'auto' follows the OS setting, 'always', 'never'
performance: 'auto' // 'auto' | 'high' | 'low'
}
- Reduced motion: ambient scene motion, the intro and the sheen are turned off. Content still updates, without movement.
- Low performance mode removes the panel blur and all continuous ambient animation (drift, fog, searchlights, grain) and keeps the look.
autopickslowon machines reporting ≤ 4 CPU threads or ≤ 4 GB memory. This is a heuristic, so set'low'explicitly if your players have weak PCs. - Animations pause automatically when the window is hidden.
NullSect credit / white-label
developer: { showCredit: true }
The small "Crafted by NullSect" signature sits in the bottom-right corner. Set showCredit: false for a fully white-label screen.
Interface options
ui: {
showAudioControls: true, // false = music plays (if enabled) without the player UI; M still mutes
showKeyboardHints: true, // the "M Mute · ← → Tips" hints
keyboardShortcuts: true
}
Preview in a browser
You can design and test without FiveM:
-
Open
html/index.htmlin Chrome or Edge, adding?preview=1to the address:file:///C:/path/to/ns-loading/html/index.html?preview=1Or serve the
htmlfolder with any static server (e.g.npx serve html) and openhttp://localhost:3000/?preview=1. -
An amber PREVIEW · Simulated loading data bar appears with tools: restart, pause, No data (the state before FiveM sends anything), language switch, sample player name and a fade-out test. It also shows a config warning count, and the details are printed in the console (F12).
The simulator sends messages in FiveM's exact format through the same code path the game uses. It only exists with ?preview=1. FiveM never opens the page with that flag, so production can never show simulated data. To disable the flag entirely, set preview.allowUrlFlag: false.
URL options: lang=tr, sim=0 (start without data), speed=2 (faster simulation), name=Alex, reduced=1 (reduced motion), perf=low.
Without ?preview=1, a browser shows exactly what FiveM shows before loading data arrives: the standby state. That is expected.